204 lines
9.6 KiB
TypeScript
204 lines
9.6 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|||
|
|
import { History, Zap, Building2, Calendar, X, Plus } from 'lucide-react';
|
|||
|
|
import { Outage, Building } from '../../types';
|
|||
|
|
import { backendApi } from '../../services/apiClient';
|
|||
|
|
import { CreateOutageCard } from './CreateOutageCard';
|
|||
|
|
import { OutageCardDetail } from './OutageCardDetail';
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
/** В режиме "по дому" передать buildingId — показываем только по этому дому и кнопка История по этому дому */
|
|||
|
|
buildingId?: string;
|
|||
|
|
/** Компактный вид для блока в сводке дома */
|
|||
|
|
compact?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const OutagesJournal: React.FC<Props> = ({ buildingId, compact }) => {
|
|||
|
|
const [activeOutages, setActiveOutages] = useState<Outage[]>([]);
|
|||
|
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|||
|
|
const [historyOutages, setHistoryOutages] = useState<Outage[]>([]);
|
|||
|
|
const [historyBuildingFilter, setHistoryBuildingFilter] = useState<string>(buildingId || '');
|
|||
|
|
const [buildings, setBuildings] = useState<Building[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [historyLoading, setHistoryLoading] = useState(false);
|
|||
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|||
|
|
const [openOutageId, setOpenOutageId] = useState<number | null>(null);
|
|||
|
|
|
|||
|
|
const fetchActive = () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
backendApi
|
|||
|
|
.getOutages(buildingId ? { buildingId, active: true } : { active: true })
|
|||
|
|
.then(setActiveOutages)
|
|||
|
|
.catch(() => setActiveOutages([]))
|
|||
|
|
.finally(() => setLoading(false));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
fetchActive();
|
|||
|
|
}, [buildingId]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (historyOpen) {
|
|||
|
|
backendApi.getBuildings().then(setBuildings).catch(() => setBuildings([]));
|
|||
|
|
if (!buildingId) setHistoryBuildingFilter('');
|
|||
|
|
}
|
|||
|
|
}, [historyOpen, buildingId]);
|
|||
|
|
|
|||
|
|
const loadHistory = () => {
|
|||
|
|
setHistoryLoading(true);
|
|||
|
|
const filter = buildingId || historyBuildingFilter;
|
|||
|
|
const params = filter ? { buildingId: filter } : undefined;
|
|||
|
|
backendApi
|
|||
|
|
.getOutages(params)
|
|||
|
|
.then(setHistoryOutages)
|
|||
|
|
.catch(() => setHistoryOutages([]))
|
|||
|
|
.finally(() => setHistoryLoading(false));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (historyOpen) loadHistory();
|
|||
|
|
}, [historyOpen, historyBuildingFilter, buildingId]);
|
|||
|
|
|
|||
|
|
const formatDate = (s: string) => {
|
|||
|
|
const d = new Date(s);
|
|||
|
|
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const title = buildingId ? 'Журнал отключений' : 'Журнал отключений';
|
|||
|
|
const subtitle = buildingId ? 'По этому дому' : 'Активные по всем домам';
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-4 animate-fade-in">
|
|||
|
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-black text-slate-500 text-[10px] uppercase tracking-[0.2em] px-1">{title}</h4>
|
|||
|
|
<p className="text-xs text-slate-400 mt-0.5">{subtitle}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => setCreateOpen(true)}
|
|||
|
|
className="inline-flex items-center gap-2 px-3 py-2 text-[10px] font-black uppercase tracking-wider text-white bg-primary-600 hover:bg-primary-700 rounded-xl transition-colors shadow-sm"
|
|||
|
|
>
|
|||
|
|
<Plus className="w-3.5 h-3.5" /> Новая запись
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => setHistoryOpen(true)}
|
|||
|
|
className="inline-flex items-center gap-2 px-3 py-2 text-xs font-medium text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-xl transition-colors"
|
|||
|
|
>
|
|||
|
|
<History className="w-3.5 h-3.5" /> История
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{loading ? (
|
|||
|
|
<p className="text-sm text-slate-400 py-6">Загрузка…</p>
|
|||
|
|
) : activeOutages.length === 0 ? (
|
|||
|
|
<p className="text-sm text-slate-400 py-6 italic">Нет активных отключений</p>
|
|||
|
|
) : (
|
|||
|
|
<ul className={`space-y-2 ${compact ? '' : 'space-y-3'}`}>
|
|||
|
|
{activeOutages.map((o) => (
|
|||
|
|
<li
|
|||
|
|
key={o.id}
|
|||
|
|
onClick={() => setOpenOutageId(o.id)}
|
|||
|
|
className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm flex flex-col sm:flex-row sm:items-center gap-3 cursor-pointer hover:border-primary-300 hover:shadow-md transition-all"
|
|||
|
|
>
|
|||
|
|
<div className="flex-1 min-w-0">
|
|||
|
|
{!buildingId && o.buildingAddress && (
|
|||
|
|
<p className="text-xs font-bold text-slate-500 flex items-center gap-1">
|
|||
|
|
<Building2 className="w-3.5 h-3.5" /> {o.buildingAddress}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
<p className="font-medium text-slate-800 mt-0.5">{o.category || o.type || 'Отключение'}</p>
|
|||
|
|
{o.description && <p className="text-sm text-slate-600 mt-1 line-clamp-2">{o.description}</p>}
|
|||
|
|
<p className="text-xs text-slate-400 mt-1 flex items-center gap-1">
|
|||
|
|
<Calendar className="w-3 h-3" /> с {formatDate(o.startAt)}
|
|||
|
|
{o.endAt && ` до ${formatDate(o.endAt)}`}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<span className="inline-flex items-center gap-1 text-[10px] font-black uppercase tracking-wider text-rose-700 bg-rose-100 px-2 py-1 rounded-xl shrink-0">
|
|||
|
|
<Zap className="w-3 h-3" /> Активно
|
|||
|
|
</span>
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{createOpen && (
|
|||
|
|
<CreateOutageCard
|
|||
|
|
isOpen={createOpen}
|
|||
|
|
onClose={() => setCreateOpen(false)}
|
|||
|
|
onSuccess={() => { fetchActive(); setCreateOpen(false); }}
|
|||
|
|
buildingId={buildingId}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
{openOutageId !== null && (
|
|||
|
|
<OutageCardDetail
|
|||
|
|
outageId={openOutageId}
|
|||
|
|
onClose={() => setOpenOutageId(null)}
|
|||
|
|
onUpdated={fetchActive}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Модалка История */}
|
|||
|
|
{historyOpen && (
|
|||
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in" onClick={() => setHistoryOpen(false)}>
|
|||
|
|
<div className="bg-white rounded-3xl w-full max-w-2xl max-h-[85vh] overflow-hidden shadow-2xl animate-scale-in flex flex-col" onClick={e => e.stopPropagation()}>
|
|||
|
|
<div className="p-4 border-b border-slate-200 flex justify-between items-center rounded-t-3xl">
|
|||
|
|
<h3 className="text-lg font-black text-slate-800">История отключений</h3>
|
|||
|
|
<button type="button" onClick={() => setHistoryOpen(false)} 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>
|
|||
|
|
{!buildingId && (
|
|||
|
|
<div className="px-4 py-3 border-b border-slate-100">
|
|||
|
|
<label className="block text-xs font-black text-slate-700 mb-2 uppercase tracking-wider">Дом</label>
|
|||
|
|
<select
|
|||
|
|
value={historyBuildingFilter}
|
|||
|
|
onChange={e => setHistoryBuildingFilter(e.target.value)}
|
|||
|
|
className="w-full px-4 py-2.5 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 className="flex-1 overflow-y-auto p-4">
|
|||
|
|
{historyLoading ? (
|
|||
|
|
<p className="text-sm text-slate-400 py-6">Загрузка…</p>
|
|||
|
|
) : historyOutages.length === 0 ? (
|
|||
|
|
<p className="text-sm text-slate-400 py-6 italic">Нет записей</p>
|
|||
|
|
) : (
|
|||
|
|
<ul className="space-y-3">
|
|||
|
|
{historyOutages.map((o) => (
|
|||
|
|
<li
|
|||
|
|
key={o.id}
|
|||
|
|
onClick={() => { setHistoryOpen(false); setOpenOutageId(o.id); }}
|
|||
|
|
className="bg-slate-50 rounded-xl p-4 border border-slate-100 cursor-pointer hover:border-primary-200 hover:bg-slate-50/80 transition-colors"
|
|||
|
|
>
|
|||
|
|
{!buildingId && o.buildingAddress && (
|
|||
|
|
<p className="text-xs font-bold text-slate-500 flex items-center gap-1">
|
|||
|
|
<Building2 className="w-3.5 h-3.5" /> {o.buildingAddress}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
<p className="font-medium text-slate-800 mt-0.5">{o.category || o.type || 'Отключение'}</p>
|
|||
|
|
{o.description && <p className="text-sm text-slate-600 mt-1 line-clamp-2">{o.description}</p>}
|
|||
|
|
<p className="text-xs text-slate-400 mt-1">
|
|||
|
|
{formatDate(o.startAt)}
|
|||
|
|
{o.endAt ? ` — ${formatDate(o.endAt)}` : ''}
|
|||
|
|
{o.active && <span className="ml-2 text-rose-600 font-black uppercase text-[10px]">Активно</span>}
|
|||
|
|
</p>
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|