491 lines
24 KiB
TypeScript
491 lines
24 KiB
TypeScript
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|||
|
|
import { X, Paperclip, HelpCircle, Calendar } from 'lucide-react';
|
|||
|
|
import { CreateApplicationPayload } from '../../types';
|
|||
|
|
import { backendApi } from '../../services/apiClient';
|
|||
|
|
import { Building, Employee } from '../../types';
|
|||
|
|
|
|||
|
|
type ResidentOption = { key: string; fullName: string; apartment: string; phone?: string };
|
|||
|
|
|
|||
|
|
function residentsFromBuilding(building: Building | null): ResidentOption[] {
|
|||
|
|
if (!building?.accounts?.length) return [];
|
|||
|
|
const list: ResidentOption[] = [];
|
|||
|
|
for (const acc of building.accounts) {
|
|||
|
|
const apt = acc.apartmentNumber || '';
|
|||
|
|
(acc.owners || []).forEach((o) => {
|
|||
|
|
if (o?.fullName) list.push({ key: `o:${acc.id}:${o.fullName}`, fullName: o.fullName, apartment: apt, phone: o.phone });
|
|||
|
|
});
|
|||
|
|
(acc.registered || []).forEach((r) => {
|
|||
|
|
if (r?.fullName) list.push({ key: `r:${acc.id}:${r.id}`, fullName: r.fullName, apartment: apt, phone: r.phone });
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
const seen = new Set<string>();
|
|||
|
|
return list.filter((r) => {
|
|||
|
|
const k = `${r.apartment}::${r.fullName}`.toLowerCase();
|
|||
|
|
if (seen.has(k)) return false;
|
|||
|
|
seen.add(k);
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const SOURCE_CHANNELS = [
|
|||
|
|
{ value: '', label: 'Выбрать' },
|
|||
|
|
{ value: 'call', label: 'Звонок' },
|
|||
|
|
{ value: 'website', label: 'Заявка с сайта' },
|
|||
|
|
{ value: 'reception', label: 'Личный приём' },
|
|||
|
|
{ value: 'mobile', label: 'Мобильное приложение' },
|
|||
|
|
{ value: 'other', label: 'Другое' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const WORK_TYPES = [
|
|||
|
|
{ value: '', label: 'Выбрать' },
|
|||
|
|
{ value: 'plumbing', label: 'Сантехника' },
|
|||
|
|
{ value: 'electrical', label: 'Электрика' },
|
|||
|
|
{ value: 'heating', label: 'Отопление' },
|
|||
|
|
{ value: 'elevator', label: 'Лифт' },
|
|||
|
|
{ value: 'common', label: 'Общедомовое имущество' },
|
|||
|
|
{ value: 'other', label: 'Другое' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const PROBLEM_DETAIL_OPTIONS = [
|
|||
|
|
{ value: '', label: 'Выбрать' },
|
|||
|
|
{ value: 'leak', label: 'Протечка' },
|
|||
|
|
{ value: 'no_water', label: 'Нет воды' },
|
|||
|
|
{ value: 'no_heat', label: 'Нет отопления' },
|
|||
|
|
{ value: 'no_power', label: 'Нет электричества' },
|
|||
|
|
{ value: 'blockage', label: 'Засор' },
|
|||
|
|
{ value: 'other', label: 'Другое' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const MAX_DESCRIPTION_LENGTH = 700;
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
isOpen: boolean;
|
|||
|
|
onClose: () => void;
|
|||
|
|
onSuccess: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const CreateApplicationCard: React.FC<Props> = ({ isOpen, onClose, onSuccess }) => {
|
|||
|
|
const [buildingId, setBuildingId] = useState('');
|
|||
|
|
const [address, setAddress] = useState('');
|
|||
|
|
const [apartment, setApartment] = useState('');
|
|||
|
|
const [sourceChannel, setSourceChannel] = useState('');
|
|||
|
|
const [isFromResident, setIsFromResident] = useState(true);
|
|||
|
|
const [contactPhone, setContactPhone] = useState('');
|
|||
|
|
const [contactName, setContactName] = useState('');
|
|||
|
|
const [selectedResidentKey, setSelectedResidentKey] = useState('');
|
|||
|
|
const [description, setDescription] = useState('');
|
|||
|
|
const [placeIncident, setPlaceIncident] = useState('');
|
|||
|
|
const [workType, setWorkType] = useState('');
|
|||
|
|
const [problemDetail, setProblemDetail] = useState('');
|
|||
|
|
const [isEmergency, setIsEmergency] = useState(false);
|
|||
|
|
const [isPaid, setIsPaid] = useState(false);
|
|||
|
|
const [isWarranty, setIsWarranty] = useState(false);
|
|||
|
|
const [deadlineAt, setDeadlineAt] = useState('');
|
|||
|
|
const [executorName, setExecutorName] = useState('');
|
|||
|
|
const [responsibleName, setResponsibleName] = useState('');
|
|||
|
|
const [observersText, setObserversText] = useState('');
|
|||
|
|
const [showInApp, setShowInApp] = useState(false);
|
|||
|
|
const [buildings, setBuildings] = useState<Building[]>([]);
|
|||
|
|
const [selectedBuilding, setSelectedBuilding] = useState<Building | null>(null);
|
|||
|
|
const [employees, setEmployees] = useState<Employee[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [error, setError] = useState<string | null>(null);
|
|||
|
|
|
|||
|
|
const residents = useMemo(() => residentsFromBuilding(selectedBuilding), [selectedBuilding]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (isOpen) {
|
|||
|
|
backendApi.getBuildings().then(setBuildings).catch(() => setBuildings([]));
|
|||
|
|
backendApi.getEmployees().then(setEmployees).catch(() => setEmployees([]));
|
|||
|
|
const defaultDeadline = new Date();
|
|||
|
|
defaultDeadline.setDate(defaultDeadline.getDate() + 8);
|
|||
|
|
setDeadlineAt(defaultDeadline.toISOString().slice(0, 10));
|
|||
|
|
setBuildingId('');
|
|||
|
|
setSelectedBuilding(null);
|
|||
|
|
setAddress('');
|
|||
|
|
setPlaceIncident('');
|
|||
|
|
setSelectedResidentKey('');
|
|||
|
|
setContactName('');
|
|||
|
|
setContactPhone('');
|
|||
|
|
setApartment('');
|
|||
|
|
}
|
|||
|
|
}, [isOpen]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!buildingId) {
|
|||
|
|
setSelectedBuilding(null);
|
|||
|
|
setAddress('');
|
|||
|
|
setPlaceIncident('');
|
|||
|
|
setSelectedResidentKey('');
|
|||
|
|
setContactName('');
|
|||
|
|
setContactPhone('');
|
|||
|
|
setApartment('');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
backendApi.getBuilding(buildingId).then((b) => {
|
|||
|
|
setSelectedBuilding(b);
|
|||
|
|
setAddress((b.passport as any)?.address || buildingId);
|
|||
|
|
setPlaceIncident(buildingId);
|
|||
|
|
}).catch(() => {
|
|||
|
|
setSelectedBuilding(null);
|
|||
|
|
setAddress('');
|
|||
|
|
setPlaceIncident('');
|
|||
|
|
});
|
|||
|
|
setSelectedResidentKey('');
|
|||
|
|
setContactName('');
|
|||
|
|
setContactPhone('');
|
|||
|
|
setApartment('');
|
|||
|
|
}, [buildingId]);
|
|||
|
|
|
|||
|
|
if (!isOpen) return null;
|
|||
|
|
|
|||
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
setError(null);
|
|||
|
|
if (!buildingId || !address.trim()) {
|
|||
|
|
setError('Выберите адрес (дом)');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (isFromResident && !selectedResidentKey) {
|
|||
|
|
setError('Выберите заявителя (жителя/собственника)');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!description.trim()) {
|
|||
|
|
setError('Опишите проблему');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|||
|
|
setError(`Проблема не более ${MAX_DESCRIPTION_LENGTH} символов`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
const placeBuilding = buildings.find(b => b.id === placeIncident) || selectedBuilding;
|
|||
|
|
const payload: CreateApplicationPayload = {
|
|||
|
|
address: address.trim(),
|
|||
|
|
description: description.trim(),
|
|||
|
|
apartment: apartment.trim() || undefined,
|
|||
|
|
sourceChannel: sourceChannel || undefined,
|
|||
|
|
isFromResident,
|
|||
|
|
contactPhone: contactPhone.trim() || undefined,
|
|||
|
|
contactName: contactName.trim() || undefined,
|
|||
|
|
placeIncident: placeBuilding?.passport?.address || placeIncident || undefined,
|
|||
|
|
workType: workType || undefined,
|
|||
|
|
problemDetail: problemDetail || undefined,
|
|||
|
|
isEmergency,
|
|||
|
|
isPaid,
|
|||
|
|
isWarranty,
|
|||
|
|
deadlineAt: deadlineAt ? new Date(deadlineAt).toISOString() : undefined,
|
|||
|
|
executorName: executorName.trim() || undefined,
|
|||
|
|
responsibleName: responsibleName.trim() || undefined,
|
|||
|
|
observersText: observersText.trim() || undefined,
|
|||
|
|
showInApp,
|
|||
|
|
buildingId: buildingId || undefined,
|
|||
|
|
};
|
|||
|
|
await backendApi.createApplication(payload);
|
|||
|
|
window.dispatchEvent(new CustomEvent('mkd-applications-changed'));
|
|||
|
|
onSuccess();
|
|||
|
|
onClose();
|
|||
|
|
} catch (err: any) {
|
|||
|
|
setError(err.message || 'Ошибка при создании заявки');
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleClose = () => {
|
|||
|
|
if (!loading) onClose();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in" onClick={handleClose}>
|
|||
|
|
<div className="bg-white rounded-3xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl animate-scale-in" 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 rounded-t-3xl z-10">
|
|||
|
|
<h3 className="text-lg font-black text-slate-800">Новая заявка</h3>
|
|||
|
|
<button type="button" onClick={handleClose} className="p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-600 rounded-xl transition-colors disabled:opacity-50">
|
|||
|
|
<X className="w-5 h-5" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
|||
|
|
{error && (
|
|||
|
|
<div className="bg-red-50 border border-red-200 text-red-600 text-sm px-4 py-3 rounded-xl">
|
|||
|
|
{error}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Откуда поступила заявка * */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">
|
|||
|
|
Откуда поступила заявка <span className="text-red-500">*</span>
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={sourceChannel}
|
|||
|
|
onChange={e => setSourceChannel(e.target.value)}
|
|||
|
|
className="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
>
|
|||
|
|
{SOURCE_CHANNELS.map(opt => (
|
|||
|
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Адрес (дом) * */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">
|
|||
|
|
Адрес (дом) <span className="text-red-500">*</span>
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={buildingId}
|
|||
|
|
onChange={e => setBuildingId(e.target.value)}
|
|||
|
|
className="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
>
|
|||
|
|
<option value="">Выбрать дом</option>
|
|||
|
|
{buildings.map(b => (
|
|||
|
|
<option key={b.id} value={b.id}>{(b.passport as any)?.address || b.id}</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Заявка от жителя / не от жителя */}
|
|||
|
|
<div>
|
|||
|
|
<div className="flex p-1 bg-slate-200/50 rounded-2xl gap-1">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => setIsFromResident(true)}
|
|||
|
|
className={`flex-1 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all ${isFromResident ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
|||
|
|
>
|
|||
|
|
Заявка от жителя
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => setIsFromResident(false)}
|
|||
|
|
className={`flex-1 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all ${!isFromResident ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
|||
|
|
>
|
|||
|
|
Заявка не от жителя
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
{isFromResident ? (
|
|||
|
|
<div className="mt-4">
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">Заявитель (житель/собственник) <span className="text-red-500">*</span></label>
|
|||
|
|
<select
|
|||
|
|
value={selectedResidentKey}
|
|||
|
|
onChange={e => {
|
|||
|
|
const key = e.target.value;
|
|||
|
|
setSelectedResidentKey(key);
|
|||
|
|
const r = residents.find(x => x.key === key);
|
|||
|
|
if (r) {
|
|||
|
|
setContactName(r.fullName);
|
|||
|
|
setContactPhone(r.phone || '');
|
|||
|
|
setApartment(r.apartment);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
>
|
|||
|
|
<option value="">{buildingId ? 'Выбрать жителя или собственника' : 'Сначала выберите адрес (дом)'}</option>
|
|||
|
|
{residents.map(r => (
|
|||
|
|
<option key={r.key} value={r.key}>{r.fullName}, кв. {r.apartment}{r.phone ? ` — ${r.phone}` : ''}</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
{selectedResidentKey && (contactPhone || contactName) && (
|
|||
|
|
<p className="text-xs text-slate-500 mt-1">
|
|||
|
|
{contactName}{contactPhone ? ` · ${contactPhone}` : ''}{apartment ? ` · кв. ${apartment}` : ''}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">Телефон</label>
|
|||
|
|
<div className="flex">
|
|||
|
|
<span className="inline-flex items-center px-3 py-2 bg-slate-100 border border-r-0 border-slate-200 rounded-l-xl text-sm text-slate-600">+7</span>
|
|||
|
|
<input
|
|||
|
|
type="tel"
|
|||
|
|
value={contactPhone}
|
|||
|
|
onChange={e => setContactPhone(e.target.value)}
|
|||
|
|
placeholder="900 123-45-67"
|
|||
|
|
className="flex-1 px-4 py-2 border border-slate-200 rounded-r-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">ФИО</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={contactName}
|
|||
|
|
onChange={e => setContactName(e.target.value)}
|
|||
|
|
placeholder="Фамилия Имя Отчество"
|
|||
|
|
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Проблема * */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">
|
|||
|
|
Проблема <span className="text-red-500">*</span>
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
value={description}
|
|||
|
|
onChange={e => setDescription(e.target.value.slice(0, MAX_DESCRIPTION_LENGTH))}
|
|||
|
|
placeholder="Опишите подробности"
|
|||
|
|
rows={4}
|
|||
|
|
className="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 resize-y"
|
|||
|
|
/>
|
|||
|
|
<p className="text-right text-xs text-slate-500 mt-1">{description.length}/{MAX_DESCRIPTION_LENGTH}</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Добавить файл */}
|
|||
|
|
<div>
|
|||
|
|
<button type="button" className="inline-flex items-center gap-2 px-4 py-2.5 border border-dashed border-slate-300 rounded-xl text-xs font-black text-slate-600 uppercase tracking-wider hover:bg-slate-50 transition-colors">
|
|||
|
|
<Paperclip className="w-4 h-4" /> Добавить файл
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Классификатор */}
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<h4 className="text-sm font-black text-slate-800 uppercase tracking-wider">Классификатор</h4>
|
|||
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">Место инцидента *</label>
|
|||
|
|
<select
|
|||
|
|
value={placeIncident}
|
|||
|
|
onChange={e => setPlaceIncident(e.target.value)}
|
|||
|
|
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
>
|
|||
|
|
<option value="">Выбрать</option>
|
|||
|
|
{buildings.map(b => (
|
|||
|
|
<option key={b.id} value={b.id}>{b.passport?.address || b.id}</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">Тип работ *</label>
|
|||
|
|
<select
|
|||
|
|
value={workType}
|
|||
|
|
onChange={e => setWorkType(e.target.value)}
|
|||
|
|
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
>
|
|||
|
|
{WORK_TYPES.map(opt => (
|
|||
|
|
<option key={opt.value || 'empty'} value={opt.value}>{opt.label}</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">В чём проблема (не обязательно)</label>
|
|||
|
|
<select
|
|||
|
|
value={problemDetail}
|
|||
|
|
onChange={e => setProblemDetail(e.target.value)}
|
|||
|
|
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
>
|
|||
|
|
{PROBLEM_DETAIL_OPTIONS.map(opt => (
|
|||
|
|
<option key={opt.value || 'empty'} value={opt.value}>{opt.label}</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex flex-wrap gap-4">
|
|||
|
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|||
|
|
<input type="checkbox" checked={isEmergency} onChange={e => setIsEmergency(e.target.checked)} className="rounded border-slate-300 text-primary-600" />
|
|||
|
|
<span className="text-xs font-bold text-slate-700">Аварийная</span>
|
|||
|
|
</label>
|
|||
|
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|||
|
|
<input type="checkbox" checked={isPaid} onChange={e => setIsPaid(e.target.checked)} className="rounded border-slate-300 text-primary-600" />
|
|||
|
|
<span className="text-xs font-bold text-slate-700">Платная</span>
|
|||
|
|
</label>
|
|||
|
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|||
|
|
<input type="checkbox" checked={isWarranty} onChange={e => setIsWarranty(e.target.checked)} className="rounded border-slate-300 text-primary-600" />
|
|||
|
|
<span className="text-xs font-bold text-slate-700">Гарантийная</span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Срок выполнения */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">
|
|||
|
|
Выполнить до… <span className="text-red-500">*</span>
|
|||
|
|
</label>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<input
|
|||
|
|
type="date"
|
|||
|
|
value={deadlineAt}
|
|||
|
|
onChange={e => setDeadlineAt(e.target.value)}
|
|||
|
|
className="flex-1 px-4 py-3 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
/>
|
|||
|
|
<Calendar className="w-5 h-5 text-slate-400" />
|
|||
|
|
</div>
|
|||
|
|
<p className="text-xs text-slate-500 mt-1 flex items-center gap-1">
|
|||
|
|
<HelpCircle className="w-3.5 h-3.5" /> Дата определена автоматически (+8 дн.)
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Назначение заявки */}
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<h4 className="text-sm font-black text-slate-800 uppercase tracking-wider">Назначение заявки</h4>
|
|||
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider flex items-center gap-1">
|
|||
|
|
Исполнитель <HelpCircle className="w-3.5 h-3.5 text-slate-400" />
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={executorName}
|
|||
|
|
onChange={e => setExecutorName(e.target.value)}
|
|||
|
|
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
>
|
|||
|
|
<option value="">Выбрать</option>
|
|||
|
|
{employees.map(emp => (
|
|||
|
|
<option key={emp.id} value={emp.name}>{emp.name}</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider flex items-center gap-1">
|
|||
|
|
Ответственный <HelpCircle className="w-3.5 h-3.5 text-slate-400" />
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={responsibleName}
|
|||
|
|
onChange={e => setResponsibleName(e.target.value)}
|
|||
|
|
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
>
|
|||
|
|
<option value="">Выбрать</option>
|
|||
|
|
{employees.map(emp => (
|
|||
|
|
<option key={emp.id} value={emp.name}>{emp.name}</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider flex items-center gap-1">
|
|||
|
|
Наблюдатели <HelpCircle className="w-3.5 h-3.5 text-slate-400" />
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={observersText}
|
|||
|
|
onChange={e => setObserversText(e.target.value)}
|
|||
|
|
placeholder="ФИО через запятую"
|
|||
|
|
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|||
|
|
<input type="checkbox" checked={showInApp} onChange={e => setShowInApp(e.target.checked)} className="rounded border-slate-300 text-primary-600" />
|
|||
|
|
<span className="text-xs font-bold text-slate-700">Отображать заявку в мобильном приложении жителя</span>
|
|||
|
|
<HelpCircle className="w-3.5 h-3.5 text-slate-400" />
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex gap-3 pt-4 border-t border-slate-200">
|
|||
|
|
<button type="button" onClick={handleClose} disabled={loading} className="flex-1 px-4 py-3 bg-slate-100 text-slate-700 font-black text-sm rounded-xl hover:bg-slate-200 transition-colors disabled:opacity-50">
|
|||
|
|
Отмена
|
|||
|
|
</button>
|
|||
|
|
<button type="submit" disabled={loading} className="flex-1 px-4 py-3 bg-primary-600 text-white font-black text-sm rounded-xl hover:bg-primary-700 disabled:opacity-50 transition-colors">
|
|||
|
|
{loading ? 'Создание…' : 'Создать заявку'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|