Files
mkd/components/development/MarketingCampaigns.tsx
2026-02-04 00:17:04 +05:00

226 lines
13 KiB
TypeScript
Executable File
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, 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>
);