krona-front/src/features/crm/components/CrmKanbanView.tsx

159 lines
6.7 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 { Deal, DealStage } from '../../../entities/crm/deal';
import { Contact } from '../../../entities/crm/contact';
import { Company } from '../../../entities/crm/company';
import { Button } from '../../../shared/ui/Button';
import { formatCurrencyMinor } from '../../../shared/lib/currency';
import { DEAL_STAGES } from '../constants';
import { Search, Plus, ArrowRight, Building } from 'lucide-react';
export interface CrmKanbanViewProps {
deals: Deal[];
contacts: Contact[];
companies: Company[];
search: string;
onSearchChange: (value: string) => void;
onAddDeal: () => void;
onStageChange: (dealId: string, nextStage: DealStage) => Promise<void> | void;
}
export const CrmKanbanView: React.FC<CrmKanbanViewProps> = ({
deals,
contacts,
companies,
search,
onSearchChange,
onAddDeal,
onStageChange,
}) => {
const [draggedDealId, setDraggedDealId] = useState<string | null>(null);
const handleDragStart = (e: React.DragEvent, dealId: string) => {
e.dataTransfer.setData('text/plain', dealId);
setDraggedDealId(dealId);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const handleDrop = async (e: React.DragEvent, targetStage: DealStage) => {
e.preventDefault();
const dealId = e.dataTransfer.getData('text/plain') || draggedDealId;
if (dealId) {
await onStageChange(dealId, targetStage);
}
setDraggedDealId(null);
};
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="relative w-64">
<Search className="w-3.5 h-3.5 absolute left-2.5 top-2.5 text-krona-secondary" />
<input
type="text"
value={search}
onChange={(e) => onSearchChange(e.target.value)}
placeholder="Поиск по сделкам..."
className="w-full pl-8 pr-3 py-1.5 text-xs bg-krona-surface border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green"
/>
</div>
<Button variant="primary" size="sm" onClick={onAddDeal}>
<Plus className="w-3.5 h-3.5 mr-1" /> Новая сделка
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-7 gap-2.5 overflow-x-auto pb-4">
{DEAL_STAGES.map((col) => {
const colDeals = deals.filter(
(d) =>
d.stage === col.key &&
(d.title.toLowerCase().includes(search.toLowerCase()) || !search)
);
const colTotalMinor = colDeals.reduce((sum, d) => sum + d.amount.minor, 0);
return (
<div
key={col.key}
data-testid={`kanban-column-${col.key}`}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, col.key)}
className={`bg-krona-surface/70 border rounded-xl p-2.5 flex flex-col min-w-[200px] max-h-[750px] shadow-krona-soft transition-all ${col.color}`}
>
<div className="flex items-center justify-between border-b border-krona-border/60 pb-2 mb-2">
<div>
<div className="text-xs font-bold text-krona-primary">{col.label}</div>
<div className="text-[11px] font-medium text-krona-green mt-0.5">
{formatCurrencyMinor(colTotalMinor)}
</div>
</div>
<span className="text-[11px] px-1.5 py-0.5 rounded-full bg-krona-workspace text-krona-secondary font-bold">
{colDeals.length}
</span>
</div>
<div className="space-y-2 overflow-y-auto flex-1 pr-0.5">
{colDeals.map((deal) => {
const contact = contacts.find((c) => c.id === deal.contactId);
const company = companies.find((c) => c.id === deal.companyId);
const nextStageIdx = DEAL_STAGES.findIndex((s) => s.key === deal.stage) + 1;
const nextStage = DEAL_STAGES[nextStageIdx]?.key;
return (
<div
key={deal.id}
data-testid={`deal-card-${deal.id}`}
data-deal-id={deal.id}
data-deal-stage={deal.stage}
draggable
onDragStart={(e) => handleDragStart(e, deal.id)}
className="bg-krona-workspace border border-krona-border hover:border-krona-green/60 p-2.5 rounded-lg text-xs space-y-2 shadow-xs cursor-grab active:cursor-grabbing transition-all"
>
<div className="font-semibold text-krona-primary line-clamp-2">
{deal.title}
</div>
<div className="flex items-center justify-between text-[11px]">
<span className="font-bold text-krona-primary">
{formatCurrencyMinor(deal.amount.minor)}
</span>
<span className="px-1.5 py-0.2 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded font-medium">
{deal.probability}% вер.
</span>
</div>
{(contact || company) && (
<div className="text-[11px] text-krona-secondary truncate flex items-center gap-1">
<Building className="w-3 h-3" />
{company?.name || `${contact?.firstName} ${contact?.lastName}`}
</div>
)}
{/* Quick stage transition button */}
{nextStage && deal.stage !== 'won' && deal.stage !== 'lost' && (
<button
type="button"
onClick={() => onStageChange(deal.id, nextStage)}
className="w-full mt-1 py-1 text-[11px] bg-krona-surface hover:bg-krona-green/10 text-krona-secondary hover:text-krona-green border border-krona-border rounded flex items-center justify-center gap-1 transition-colors"
>
Далее: {DEAL_STAGES[nextStageIdx]?.label} <ArrowRight className="w-3 h-3" />
</button>
)}
</div>
);
})}
{colDeals.length === 0 && (
<div className="text-center py-6 text-[11px] text-krona-secondary italic">
Пусто
</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
};