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

148 lines
7.9 KiB
TypeScript
Executable File
Raw Permalink 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, useMemo } from 'react';
import { DomaApplication, DomaApplicationStatus } from '../../types';
import { Search, Clock, User, HardHat, ExternalLink, RotateCw } from 'lucide-react';
import { ApplicationCardDetail } from './ApplicationCardDetail';
interface Props {
applications: DomaApplication[];
onRefresh?: () => void;
}
const statusConfig = {
new: { label: 'Новая', color: 'bg-red-500', textColor: 'text-red-600', bgColor: 'bg-red-100' },
in_progress: { label: 'В работе', color: 'bg-blue-500', textColor: 'text-blue-600', bgColor: 'bg-blue-100' },
deferred: { label: 'Отложена', color: 'bg-orange-500', textColor: 'text-orange-600', bgColor: 'bg-orange-100' },
done: { label: 'Выполнена', color: 'bg-emerald-500', textColor: 'text-emerald-600', bgColor: 'bg-emerald-100' },
canceled: { label: 'Отменена', color: 'bg-slate-400', textColor: 'text-slate-600', bgColor: 'bg-slate-100' },
};
export const ApplicationsRegistry: React.FC<Props> = ({ applications, onRefresh }) => {
const [search, setSearch] = useState('');
const [filter, setFilter] = useState<DomaApplicationStatus | 'all'>('all');
const [openCardId, setOpenCardId] = useState<number | null>(null);
const filtered = useMemo(() => {
return applications.filter(app => {
const matchesSearch = app.address.toLowerCase().includes(search.toLowerCase()) || app.number.toLowerCase().includes(search.toLowerCase());
const matchesFilter = filter === 'all' || app.status === filter;
return matchesSearch && matchesFilter;
}).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
}, [applications, search, filter]);
return (
<div className="space-y-4 animate-fade-in">
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1 flex gap-2">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Поиск по адресу, номеру заявки..."
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full pl-11 pr-4 py-3 bg-white border border-slate-200 rounded-2xl text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm"
/>
{onRefresh && (
<button
type="button"
onClick={() => window.dispatchEvent(new CustomEvent('mkd-applications-changed'))}
className="shrink-0 p-3 bg-white border border-slate-200 rounded-2xl text-slate-400 hover:text-primary-600 hover:border-primary-300 transition-all"
title="Обновить реестр"
>
<RotateCw className="w-5 h-5" />
</button>
)}
</div>
<div className="flex bg-slate-200/50 p-1 rounded-2xl shrink-0 overflow-x-auto no-scrollbar">
{['all', 'new', 'in_progress', 'deferred', 'done'].map((f) => (
<button
key={f}
onClick={() => setFilter(f as any)}
className={`flex-shrink-0 min-w-[5rem] px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all whitespace-nowrap ${filter === f ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
>
{f === 'all' ? 'Все' : f === 'new' ? 'Новые' : f === 'in_progress' ? 'В работе' : f === 'deferred' ? 'Отложены' : 'Готово'}
</button>
))}
</div>
</div>
<div className="space-y-3">
{filtered.map(app => (
<ApplicationCard key={app.id} application={app} onOpenCard={() => setOpenCardId(app.id)} />
))}
{filtered.length === 0 && (
<div className="py-20 text-center text-slate-400 italic">Заявки не найдены</div>
)}
</div>
{openCardId !== null && (
<ApplicationCardDetail
applicationId={openCardId}
onClose={() => setOpenCardId(null)}
onUpdated={onRefresh}
/>
)}
</div>
);
};
const ApplicationCard: React.FC<{ application: DomaApplication; onOpenCard: () => void }> = ({ application, onOpenCard }) => {
const { label, color, textColor, bgColor } = statusConfig[application.status];
const timeSince = (dateStr: string) => {
const date = new Date(dateStr);
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
if (seconds < 3600) return Math.floor(seconds/60) + " мин. назад";
if (seconds < 86400) return Math.floor(seconds/3600) + " ч. назад";
return Math.floor(seconds/86400) + " д. назад";
};
const handleCardClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (target.closest('a[href]')) return;
onOpenCard();
};
return (
<div
onClick={handleCardClick}
className="bg-white p-5 rounded-[2rem] border border-slate-200 shadow-sm hover:shadow-md hover:border-primary-300 transition-all cursor-pointer group relative overflow-hidden"
>
<div className={`absolute left-0 top-0 bottom-0 w-1.5 ${color}`} />
<div className="pl-3">
<div className="flex justify-between items-start mb-3">
<div>
<span className={`px-2 py-0.5 text-[9px] font-black rounded-full uppercase tracking-tighter ${bgColor} ${textColor}`}>{label}</span>
<p className="text-[10px] text-slate-400 font-bold mt-1 uppercase"> {application.number}</p>
</div>
{application.domaId && (
<a
href={`https://condo.d.doma.ai/ticket/${application.domaId}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="flex items-center gap-1 text-slate-400 group-hover:text-primary-600 transition-colors"
>
<ExternalLink className="w-4 h-4" />
<span className="text-[9px] font-bold uppercase">Открыть в Doma.AI</span>
</a>
)}
</div>
<h3 className="font-black text-slate-800 text-sm mb-1 group-hover:text-primary-600 transition-colors">{application.address}, кв. {application.apartment}</h3>
<p className="text-xs text-slate-500 leading-relaxed line-clamp-2">{application.description}</p>
<div className="mt-4 pt-4 border-t border-slate-50 flex flex-wrap gap-4 text-[10px] text-slate-400 font-bold uppercase tracking-tight">
<div className="flex items-center gap-1.5"><Clock className="w-3 h-3" /> {timeSince(application.createdAt)}</div>
<div className="flex items-center gap-1.5"><User className="w-3 h-3" /> {application.clientName}</div>
{application.performer && (
<div className="flex items-center gap-1.5 text-primary-600">
<HardHat className="w-3 h-3"/> {application.performer.name}
</div>
)}
</div>
</div>
</div>
);
};