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

377 lines
19 KiB
TypeScript
Executable File
Raw Permalink 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 } from 'react';
import { X } from 'lucide-react';
import { DevMarketingActivity } from '../../types';
import { Building } from '../../types';
import { backendApi } from '../../services/apiClient';
interface Props {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
activity?: DevMarketingActivity | null;
}
export const MarketingActivityModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, activity }) => {
const [formData, setFormData] = useState({
buildingId: '',
address: '',
activistsCount: 0,
meetingsHeld: 0,
adsDistributed: 0,
competitor: '',
status: 'voting' as 'voting' | 'my_house' | 'competitor_house',
notes: '',
source: 'existing' as 'existing' | 'pipeline',
});
const [buildings, setBuildings] = useState<Building[]>([]);
const [pipelineAddresses, setPipelineAddresses] = useState<{id: string, address: string}[]>([]);
const [loading, setLoading] = useState(false);
const [loadingBuildings, setLoadingBuildings] = useState(true);
const isEditMode = !!activity;
useEffect(() => {
if (isOpen) {
if (activity) {
// Режим редактирования
setFormData({
buildingId: activity.buildingId || '',
address: activity.address,
activistsCount: activity.activistsCount,
meetingsHeld: activity.meetingsHeld,
adsDistributed: activity.adsDistributed,
competitor: activity.competitor || '',
status: activity.status,
notes: '',
source: activity.buildingId ? 'existing' : 'pipeline',
});
}
fetchData();
}
}, [isOpen, activity]);
const fetchData = async () => {
try {
setLoadingBuildings(true);
const buildingsData = await backendApi.getBuildings();
setBuildings(buildingsData);
try {
const pipelineData = await backendApi.getDevelopmentPipeline();
setPipelineAddresses(pipelineData.map(p => ({ id: p.id, address: p.address })));
} catch (err) {
console.warn('Failed to load pipeline addresses:', err);
}
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoadingBuildings(false);
}
};
if (!isOpen) return null;
const handleSourceChange = (source: 'existing' | 'pipeline') => {
setFormData({ ...formData, source, buildingId: '', address: '' });
};
const handleBuildingChange = (value: string) => {
if (formData.source === 'existing') {
const building = buildings.find(b => b.id === value);
setFormData({
...formData,
buildingId: value,
address: building?.passport?.address || ''
});
} else {
const pipelineItem = pipelineAddresses.find(p => p.id === value);
setFormData({
...formData,
buildingId: value,
address: pipelineItem?.address || ''
});
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.address || !formData.status) {
alert('Заполните обязательные поля: адрес и статус');
return;
}
try {
setLoading(true);
if (isEditMode && activity) {
// Обновление существующей активности
await backendApi.updateDevelopmentMarketing(activity.id, {
address: formData.address,
activists_count: formData.activistsCount,
meetings_held: formData.meetingsHeld,
ads_distributed: formData.adsDistributed,
competitor: formData.competitor || null,
status: formData.status,
notes: formData.notes || null,
});
} else {
// Создание новой активности
const payload = {
address: formData.address,
building_id: formData.source === 'existing' ? formData.buildingId : null,
activists_count: formData.activistsCount,
meetings_held: formData.meetingsHeld,
ads_distributed: formData.adsDistributed,
competitor: formData.competitor || null,
status: formData.status,
notes: formData.notes || null,
};
await backendApi.createDevelopmentMarketing(payload);
}
onSuccess();
onClose();
// Сброс формы только если не режим редактирования
if (!isEditMode) {
setFormData({
buildingId: '',
address: '',
activistsCount: 0,
meetingsHeld: 0,
adsDistributed: 0,
competitor: '',
status: 'voting',
notes: '',
source: 'existing',
});
}
} catch (error: any) {
console.error('Error saving marketing activity:', error);
const errorMessage = error?.message || error?.error || 'Ошибка при сохранении';
alert(errorMessage);
} finally {
setLoading(false);
}
};
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/80 backdrop-blur-md animate-fade-in"
onClick={onClose}
>
<div
className="bg-white rounded-2xl w-full max-w-2xl shadow-2xl animate-slide-up max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-white border-b border-slate-200 px-6 py-4 flex justify-between items-center">
<h3 className="text-lg font-black text-slate-800">
{isEditMode ? 'Редактировать активность' : 'Создать маркетинговую активность'}
</h3>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-xl transition-colors"
>
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{!isEditMode && (
<>
{/* Выбор источника */}
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">
Тип объекта
</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => handleSourceChange('existing')}
className={`flex-1 p-3 rounded-xl border-2 transition-all ${
formData.source === 'existing'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-slate-200 bg-white text-slate-600'
}`}
>
<div className="font-bold text-sm">Существующий дом</div>
</button>
<button
type="button"
onClick={() => handleSourceChange('pipeline')}
className={`flex-1 p-3 rounded-xl border-2 transition-all ${
formData.source === 'pipeline'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-slate-200 bg-white text-slate-600'
}`}
>
<div className="font-bold text-sm">Из воронки</div>
</button>
</div>
</div>
{/* Выбор дома/адреса */}
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">
{formData.source === 'existing' ? 'Дом *' : 'Объект из воронки *'}
</label>
{loadingBuildings ? (
<div className="w-full p-2.5 rounded-xl border border-slate-200 text-sm text-slate-400">
Загрузка...
</div>
) : (
<>
{formData.source === 'existing' ? (
<select
required={!isEditMode}
value={formData.buildingId}
onChange={(e) => handleBuildingChange(e.target.value)}
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="">Выберите дом</option>
{buildings.map(building => (
<option key={building.id} value={building.id}>
{building.passport?.address || building.id}
</option>
))}
</select>
) : (
<select
required={!isEditMode}
value={formData.buildingId}
onChange={(e) => handleBuildingChange(e.target.value)}
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="">Выберите объект из воронки</option>
{pipelineAddresses.map(item => (
<option key={item.id} value={item.id}>
{item.address}
</option>
))}
</select>
)}
</>
)}
</div>
</>
)}
{/* Адрес */}
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">
Адрес *
</label>
<input
type="text"
required
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
{/* Статус */}
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">
Статус *
</label>
<select
required
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as any })}
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="voting">Идет голосование</option>
<option value="my_house">Наш дом</option>
<option value="competitor_house">Дом конкурента</option>
</select>
</div>
{/* Конкурент */}
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">
Текущая УК / Конкурент
</label>
<input
type="text"
value={formData.competitor}
onChange={(e) => setFormData({ ...formData, competitor: e.target.value })}
placeholder="УК ЖилКом, ТСЖ Рассвет и т.д."
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
{/* Метрики */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">
Количество активистов
</label>
<input
type="number"
min="0"
value={formData.activistsCount}
onChange={(e) => setFormData({ ...formData, activistsCount: parseInt(e.target.value) || 0 })}
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">
Проведено встреч
</label>
<input
type="number"
min="0"
value={formData.meetingsHeld}
onChange={(e) => setFormData({ ...formData, meetingsHeld: parseInt(e.target.value) || 0 })}
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">
Роздано листовок
</label>
<input
type="number"
min="0"
value={formData.adsDistributed}
onChange={(e) => setFormData({ ...formData, adsDistributed: parseInt(e.target.value) || 0 })}
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
/>
</div>
</div>
{/* Примечания */}
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">
Примечания
</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
rows={3}
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none resize-none"
/>
</div>
<div className="flex gap-3 justify-end pt-4 border-t border-slate-200">
<button
type="button"
onClick={onClose}
className="px-6 py-2.5 bg-slate-100 text-slate-700 rounded-xl text-sm font-bold hover:bg-slate-200 transition-colors"
>
Отмена
</button>
<button
type="submit"
disabled={loading}
className="px-6 py-2.5 bg-primary-600 text-white rounded-xl text-sm font-bold hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Сохранение...' : isEditMode ? 'Сохранить' : 'Создать'}
</button>
</div>
</form>
</div>
</div>
);
};