Files
mkd/components/development/MarketingCampaigns.tsx

226 lines
13 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { DevMarketingActivity } from '../../types';
import { Megaphone, Users, MessageCircle, Phone, Calendar, ClipboardList, CheckCircle2, MapPin, Plus, Edit, Search, Filter } from 'lucide-react';
import { backendApi } from '../../services/apiClient';
import { MarketingActivityModal } from './MarketingActivityModal';
export const MarketingCampaigns: React.FC = () => {
const [activities, setActivities] = useState<DevMarketingActivity[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingActivity, setEditingActivity] = useState<DevMarketingActivity | null>(null);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('');
const fetchActivities = useCallback(async () => {
try {
setLoading(true);
const params = statusFilter ? { status: statusFilter } : undefined;
const data = await backendApi.getDevelopmentMarketing(params);
setActivities(data);
} catch (error) {
console.error('Error fetching marketing activities:', error);
setActivities([]);
} finally {
setLoading(false);
}
}, [statusFilter]);
useEffect(() => {
fetchActivities();
}, [fetchActivities]);
const filteredActivities = useMemo(() => {
if (!search.trim()) return activities;
const q = search.trim().toLowerCase();
return activities.filter(a => (a.address || '').toLowerCase().includes(q) || (a.competitor || '').toLowerCase().includes(q));
}, [activities, search]);
const handleCreate = () => {
setEditingActivity(null);
setIsModalOpen(true);
};
const handleEdit = (activity: DevMarketingActivity) => {
setEditingActivity(activity);
setIsModalOpen(true);
};
const handleContacts = () => {
alert('В разработке: список контактов по объекту (активисты, председатели).');
};
const handleMeetingsPlan = () => {
alert('В разработке: календарь и список запланированных встреч по объекту.');
};
const totals = useMemo(() => {
const sum = (arr: DevMarketingActivity[], key: keyof DevMarketingActivity) =>
arr.reduce((acc, a) => acc + (Number(a[key]) || 0), 0);
return {
meetings: sum(activities, 'meetingsHeld'),
ads: sum(activities, 'adsDistributed'),
activists: sum(activities, 'activistsCount'),
withMetrics: activities.filter(a => (a.meetingsHeld || 0) > 0 || (a.adsDistributed || 0) > 0 || (a.activistsCount || 0) > 0).length,
};
}, [activities]);
return (
<div className="space-y-6 animate-fade-in">
<div className="flex flex-wrap justify-between items-center gap-3 px-1">
<h3 className="font-black text-slate-500 text-[10px] uppercase tracking-[0.2em]">Работа с активом</h3>
<div className="flex flex-wrap items-center gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Поиск по адресу, УК..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9 pr-3 py-2 rounded-xl border border-slate-200 text-sm w-48 focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div className="flex items-center gap-1.5">
<Filter className="w-4 h-4 text-slate-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="text-[10px] font-bold text-slate-600 bg-white border border-slate-200 rounded-lg py-2 px-2.5 focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="">Все статусы</option>
<option value="voting">Идёт голосование</option>
<option value="my_house">Наш дом</option>
<option value="competitor_house">Дом конкурента</option>
</select>
</div>
<button
onClick={handleCreate}
className="bg-primary-600 text-white p-2.5 rounded-xl shadow-lg shadow-primary-500/30 flex items-center gap-2 px-4 text-xs font-black uppercase tracking-wider active:scale-95 transition-all"
>
<Plus className="w-4 h-4" /> Добавить активность
</button>
<button className="p-2 bg-white border border-slate-200 rounded-xl text-slate-400 hover:text-primary-600 transition-colors" title="Отчёты"><ClipboardList className="w-5 h-5"/></button>
</div>
</div>
{loading ? (
<div className="text-center py-20 text-slate-400">
<div className="w-10 h-10 border-2 border-slate-200 rounded-full mx-auto mb-2 animate-spin" />
<p className="text-[10px] font-bold uppercase tracking-widest">Загрузка...</p>
</div>
) : activities.length === 0 ? (
<div className="text-center py-20 text-slate-300">
<div className="w-10 h-10 border-2 border-dashed border-slate-200 rounded-full mx-auto mb-2" />
<p className="text-[10px] font-bold uppercase tracking-widest">Нет маркетинговых активностей</p>
<p className="text-xs text-slate-400 mt-2 max-w-sm mx-auto">Агитация и маркетинг по объектам из воронки и существующим домам.</p>
</div>
) : filteredActivities.length === 0 ? (
<div className="text-center py-12 text-slate-400">
<p className="text-[10px] font-bold uppercase tracking-widest">Нет активностей по фильтру</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredActivities.map(m => (
<div key={m.id} className="bg-white p-5 rounded-[2rem] border border-slate-200 shadow-sm relative overflow-hidden group hover:shadow-lg transition-all">
{/* Status Vertical Line */}
<div className={`absolute left-0 top-1/4 bottom-1/4 w-1.5 rounded-r-full ${m.status === 'voting' ? 'bg-amber-500' : 'bg-emerald-500'}`}/>
<div className="pl-4">
<div className="flex justify-between items-start mb-4">
<div>
<h4 className="font-black text-slate-800 text-base leading-tight group-hover:text-primary-600 transition-colors">{m.address}</h4>
<div className="flex items-center gap-1.5 mt-1">
<MapPin className="w-3 h-3 text-slate-400"/>
<p className="text-[10px] text-slate-400 font-bold uppercase">Тек. УК: {m.competitor}</p>
</div>
</div>
<button
onClick={() => handleEdit(m)}
className="p-1.5 text-slate-300 hover:text-primary-600 transition-colors"
title="Редактировать"
>
<Edit className="w-5 h-5"/>
</button>
</div>
<div className="grid grid-cols-3 gap-3 mb-6">
<MarketingMetric icon={Users} value={m.activistsCount} label="Активиста" color="text-primary-600" bg="bg-primary-50" />
<MarketingMetric icon={MessageCircle} value={m.meetingsHeld} label="Встречи" color="text-indigo-600" bg="bg-indigo-50" />
<MarketingMetric icon={Megaphone} value={m.adsDistributed} label="Листовки" color="text-amber-600" bg="bg-amber-50" />
</div>
<div className="flex gap-2 pt-4 border-t border-slate-100">
<button
onClick={handleContacts}
className="flex-1 py-2.5 bg-slate-50 text-slate-700 rounded-xl text-[10px] font-black uppercase flex items-center justify-center gap-2 hover:bg-slate-100 transition-all border border-slate-100"
>
<Phone className="w-3.5 h-3.5"/> Контакты
</button>
<button
onClick={handleMeetingsPlan}
className="flex-1 py-2.5 bg-primary-600 text-white rounded-xl text-[10px] font-black uppercase flex items-center justify-center gap-2 shadow-lg shadow-primary-500/20 active:scale-95 transition-all"
>
<Calendar className="w-3.5 h-3.5"/> План встреч
</button>
</div>
</div>
</div>
))}
</div>
)}
{/* Итоги по активностям (реальные данные) */}
<div className="bg-slate-900 rounded-[2rem] p-6 text-white shadow-xl">
<div className="flex items-center gap-2 mb-4">
<CheckCircle2 className="w-5 h-5 text-emerald-400" />
<h3 className="font-bold text-sm uppercase tracking-widest">Итоги по активностям</h3>
</div>
{activities.length === 0 ? (
<p className="text-[11px] text-slate-400">Данные по итогам месяца будут доступны после добавления активностей.</p>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white/10 rounded-xl p-4">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Встреч проведено</p>
<p className="text-2xl font-black text-white">{totals.meetings}</p>
</div>
<div className="bg-white/10 rounded-xl p-4">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Листовок роздано</p>
<p className="text-2xl font-black text-white">{totals.ads}</p>
</div>
<div className="bg-white/10 rounded-xl p-4">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Активистов</p>
<p className="text-2xl font-black text-white">{totals.activists}</p>
</div>
<div className="bg-white/10 rounded-xl p-4">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Объектов с метриками</p>
<p className="text-2xl font-black text-white">{totals.withMetrics} / {activities.length}</p>
</div>
</div>
)}
</div>
{/* Modal */}
<MarketingActivityModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setEditingActivity(null);
}}
onSuccess={() => {
fetchActivities();
}}
activity={editingActivity}
/>
</div>
);
};
const MarketingMetric = ({ icon: Icon, value, label, color, bg }: any) => (
<div className={`${bg} p-3 rounded-2xl flex flex-col items-center justify-center border border-white/50`}>
<Icon className={`w-4 h-4 ${color} mb-1`} />
<span className="text-sm font-black text-slate-800 leading-none">{value}</span>
<span className="text-[8px] font-black text-slate-400 uppercase mt-1">{label}</span>
</div>
);