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

289 lines
15 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, Trash2 } from 'lucide-react';
import { DevPipelineItem, DevPipelineStatus, Employee } from '../../types';
import { backendApi } from '../../services/apiClient';
interface Props {
item: DevPipelineItem | null;
onClose: () => void;
onSuccess: () => void;
}
const STAGES: { id: DevPipelineStatus; label: string }[] = [
{ id: 'incoming', label: 'Входящие' },
{ id: 'analysis', label: 'Анализ' },
{ id: 'agenda_approval', label: 'Согласование повестки' },
{ id: 'in_person', label: 'Очная часть' },
{ id: 'absentee', label: 'Заочная часть' },
{ id: 'protocol_formation', label: 'Формирование протокола' },
{ id: 'protocol_to_gzhi', label: 'Отправка протокола в ГЖИ' },
{ id: 'gzhi_order', label: 'Приказ ГЖИ' },
{ id: 'success', label: 'Успех' },
{ id: 'failure', label: 'Провал' },
];
export const EditPipelineObjectModal: React.FC<Props> = ({ item, onClose, onSuccess }) => {
const [formData, setFormData] = useState<Partial<DevPipelineItem>>({});
const [employees, setEmployees] = useState<Employee[]>([]);
const [loading, setLoading] = useState(false);
const [loadingEmployees, setLoadingEmployees] = useState(true);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (item) {
setFormData({
address: item.address,
type: item.type,
floors: item.floors,
area: item.area,
apartments: item.apartments,
status: item.status,
probability: item.probability,
expectedRevenue: item.expectedRevenue,
manager: item.manager,
notes: item.notes ?? '',
});
fetchEmployees();
}
}, [item]);
const fetchEmployees = async () => {
try {
setLoadingEmployees(true);
const data = await backendApi.getEmployees();
setEmployees(data.filter(emp => emp.status === 'active'));
} catch (error) {
console.error('Error fetching employees:', error);
setEmployees([]);
} finally {
setLoadingEmployees(false);
}
};
if (!item) return null;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.address || !formData.manager) {
alert('Заполните обязательные поля: адрес и менеджер');
return;
}
if ((formData.area ?? 0) <= 0 || (formData.apartments ?? 0) <= 0 || (formData.floors ?? 0) <= 0) {
alert('Площадь, количество квартир и этажность должны быть больше 0');
return;
}
try {
setLoading(true);
await backendApi.updateDevelopmentPipeline(item.id, {
address: formData.address,
type: formData.type,
floors: formData.floors,
area: formData.area,
apartments: formData.apartments,
status: formData.status,
probability: formData.probability,
expectedRevenue: formData.expectedRevenue,
manager: formData.manager,
notes: formData.notes ?? undefined,
});
window.dispatchEvent(new CustomEvent('mkd-pipeline-changed'));
onSuccess();
onClose();
} catch (error: unknown) {
const err = error as { message?: string; error?: string };
alert(err?.message || err?.error || 'Ошибка при сохранении');
} finally {
setLoading(false);
}
};
const handleDelete = async () => {
if (!confirm('Удалить объект из воронки?')) return;
try {
setDeleting(true);
await backendApi.deleteDevelopmentPipeline(item.id);
window.dispatchEvent(new CustomEvent('mkd-pipeline-changed'));
onSuccess();
onClose();
} catch (error: unknown) {
const err = error as { message?: string; error?: string };
alert(err?.message || err?.error || 'Ошибка при удалении');
} finally {
setDeleting(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">Карточка объекта</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={handleDelete}
disabled={deleting}
className="p-2 text-red-500 hover:bg-red-50 rounded-xl transition-colors disabled:opacity-50"
title="Удалить"
>
<Trash2 className="w-5 h-5" />
</button>
<button type="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>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<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 })}
placeholder="ул. Примерная, д. 1"
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
value={formData.type ?? 'old'}
onChange={(e) => setFormData({ ...formData, type: e.target.value as 'old' | 'new' })}
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="old">Вторичка</option>
<option value="new">Новостройка</option>
</select>
</div>
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">Этажность</label>
<input
type="number"
required
min={1}
value={formData.floors ?? 0}
onChange={(e) => setFormData({ ...formData, floors: parseInt(e.target.value, 10) || 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"
required
min={0}
step={0.01}
value={formData.area ?? 0}
onChange={(e) => setFormData({ ...formData, area: parseFloat(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"
required
min={0}
value={formData.apartments ?? 0}
onChange={(e) => setFormData({ ...formData, apartments: parseInt(e.target.value, 10) || 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>
<select
value={formData.status ?? 'incoming'}
onChange={(e) => setFormData({ ...formData, status: e.target.value as DevPipelineStatus })}
className="w-full p-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
>
{STAGES.map((s) => (
<option key={s.id} value={s.id}>{s.label}</option>
))}
</select>
</div>
<div>
<label className="text-[10px] text-slate-500 font-bold uppercase block mb-2">Вероятность (%)</label>
<input
type="number"
required
min={0}
max={100}
value={formData.probability ?? 0}
onChange={(e) => setFormData({ ...formData, probability: parseInt(e.target.value, 10) || 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"
required
min={0}
value={formData.expectedRevenue ?? 0}
onChange={(e) => setFormData({ ...formData, expectedRevenue: parseFloat(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>
{loadingEmployees ? (
<div className="w-full p-2.5 rounded-xl border border-slate-200 text-sm text-slate-400">Загрузка...</div>
) : (
<select
required
value={formData.manager ?? ''}
onChange={(e) => setFormData({ ...formData, manager: 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>
{employees.map((emp) => (
<option key={emp.id} value={emp.name}>
{emp.name} {emp.position ? `(${emp.position})` : ''}
</option>
))}
</select>
)}
</div>
<div className="md:col-span-2">
<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>
<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 ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
</form>
</div>
</div>
);
};