174 lines
7.6 KiB
TypeScript
Executable File
174 lines
7.6 KiB
TypeScript
Executable File
import React, { useState, useEffect } from 'react';
|
||
import { X, Calendar, MapPin, Zap, FileText, User } from 'lucide-react';
|
||
import { Outage } from '../../types';
|
||
import { backendApi } from '../../services/apiClient';
|
||
|
||
const WORK_TYPE_LABELS: Record<string, string> = {
|
||
absent: 'Отсутствует',
|
||
planned: 'Плановый',
|
||
emergency: 'Аварийный',
|
||
};
|
||
|
||
interface Props {
|
||
outageId: number;
|
||
onClose: () => void;
|
||
onUpdated?: () => void;
|
||
}
|
||
|
||
export const OutageCardDetail: React.FC<Props> = ({ outageId, onClose, onUpdated }) => {
|
||
const [outage, setOutage] = useState<Outage | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
const loadOutage = () => {
|
||
backendApi.getOutage(outageId).then(setOutage).catch(() => setOutage(null));
|
||
};
|
||
|
||
useEffect(() => {
|
||
setLoading(true);
|
||
loadOutage();
|
||
setLoading(false);
|
||
}, [outageId]);
|
||
|
||
const handleSetInactive = async () => {
|
||
if (!outage) return;
|
||
try {
|
||
await backendApi.updateOutage(outageId, { active: false });
|
||
loadOutage();
|
||
onUpdated?.();
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
};
|
||
|
||
if (loading || !outage) {
|
||
return (
|
||
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in">
|
||
<div className="bg-white rounded-3xl w-full max-w-2xl p-12 text-center text-slate-500 shadow-2xl">
|
||
{loading ? 'Загрузка…' : 'Отключение не найдено'}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const formatDate = (s: string) => {
|
||
const d = new Date(s);
|
||
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||
};
|
||
|
||
const workTypeLabel = outage.workType ? (WORK_TYPE_LABELS[outage.workType] || outage.workType) : '—';
|
||
const classifierText = [outage.category, outage.problemDetail].filter(Boolean).join(' » ') || outage.type || '—';
|
||
const address = outage.buildingAddress || outage.buildingId;
|
||
|
||
const endDate = outage.endAt ? new Date(outage.endAt) : null;
|
||
const now = new Date();
|
||
const remainingMs = endDate && endDate > now ? endDate.getTime() - now.getTime() : null;
|
||
const remainingStr = remainingMs != null
|
||
? `${String(Math.floor(remainingMs / 3600000)).padStart(2, '0')}:${String(Math.floor((remainingMs % 3600000) / 60000)).padStart(2, '0')}`
|
||
: null;
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm overflow-y-auto animate-fade-in">
|
||
<div className="bg-white rounded-3xl w-full max-w-2xl max-h-[95vh] overflow-y-auto shadow-2xl my-8 animate-scale-in">
|
||
{/* Header */}
|
||
<div className="sticky top-0 bg-white border-b border-slate-200 px-6 py-4 rounded-t-3xl z-10 flex justify-between items-start">
|
||
<div>
|
||
<h1 className="text-lg font-black text-slate-800">Отключение №{outage.id}</h1>
|
||
<p className="text-xs text-slate-500 mt-1">
|
||
Дата создания {formatDate(outage.createdAt)}, автор {outage.authorName || 'Администратор'} (вы)
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{outage.active && (
|
||
<span className="inline-flex items-center gap-1 text-[10px] font-black uppercase tracking-wider text-rose-700 bg-rose-100 px-3 py-1.5 rounded-xl">
|
||
<Zap className="w-3.5 h-3.5" /> Актуально
|
||
</span>
|
||
)}
|
||
{!outage.active && (
|
||
<span className="inline-flex items-center gap-1 text-[10px] font-black uppercase tracking-wider text-slate-600 bg-slate-200 px-3 py-1.5 rounded-xl">
|
||
Завершено
|
||
</span>
|
||
)}
|
||
<button type="button" onClick={onClose} className="p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-600 rounded-xl transition-colors">
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-6 py-4 space-y-4">
|
||
{/* Адрес */}
|
||
<div>
|
||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Адрес</label>
|
||
<p className="text-sm font-medium text-slate-800 flex items-center gap-1">
|
||
<MapPin className="w-4 h-4 text-slate-400 shrink-0" />
|
||
<span className="text-primary-600 underline cursor-pointer hover:no-underline">{address}</span>
|
||
</p>
|
||
</div>
|
||
|
||
{/* Дата проведения работ */}
|
||
<div>
|
||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Дата проведения работ</label>
|
||
<p className="text-sm font-medium text-slate-800 flex items-center gap-1 flex-wrap">
|
||
<Calendar className="w-4 h-4 text-slate-400 shrink-0" />
|
||
с {formatDate(outage.startAt)}
|
||
{outage.endAt && (
|
||
<>
|
||
{' '}до {formatDate(outage.endAt)}
|
||
{remainingStr && outage.active && (
|
||
<span className="text-orange-600 font-bold text-xs">(осталось {remainingStr})</span>
|
||
)}
|
||
</>
|
||
)}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Классификатор */}
|
||
<div>
|
||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Классификатор</label>
|
||
<p className="text-sm text-slate-800">{classifierText}</p>
|
||
</div>
|
||
|
||
{/* Тип работ */}
|
||
<div>
|
||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Тип работ</label>
|
||
<p className="text-sm text-slate-800">{workTypeLabel}</p>
|
||
</div>
|
||
|
||
{/* Что случилось */}
|
||
<div>
|
||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Что случилось</label>
|
||
<p className="text-sm text-slate-800 flex items-start gap-1">
|
||
<FileText className="w-4 h-4 text-slate-400 shrink-0 mt-0.5" />
|
||
{outage.description || '—'}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Что сказать жителю */}
|
||
<div>
|
||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-wider mb-1">Что сказать жителю</label>
|
||
<p className="text-sm text-slate-800 flex items-start gap-1">
|
||
<User className="w-4 h-4 text-slate-400 shrink-0 mt-0.5" />
|
||
{outage.residentMessage || '—'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Действия */}
|
||
<div className="sticky bottom-0 bg-white border-t border-slate-200 px-6 py-4 rounded-b-3xl flex flex-wrap gap-2">
|
||
{outage.active && (
|
||
<button
|
||
type="button"
|
||
onClick={handleSetInactive}
|
||
className="px-4 py-2.5 bg-slate-100 text-slate-700 rounded-xl font-black text-xs uppercase tracking-wider hover:bg-slate-200 transition-colors"
|
||
>
|
||
Завершить отключение
|
||
</button>
|
||
)}
|
||
<button type="button" onClick={onClose} className="px-4 py-2.5 bg-primary-600 text-white rounded-xl font-black text-xs uppercase tracking-wider hover:bg-primary-700 transition-colors">
|
||
Закрыть
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|