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