Files
mkd/components/applications/OutagesJournal.tsx

204 lines
9.6 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
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>
);
};