Files
mkd/components/applications/OutagesJournal.tsx
2026-02-04 00:17:04 +05:00

204 lines
9.6 KiB
TypeScript
Executable File
Raw 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 { 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>
);
};