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

261 lines
10 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, { useEffect, useMemo, useState } from 'react';
import { backendApi } from '../../services/apiClient';
import { Building, Employee } from '../../types';
import { AlertTriangle, Check, Loader2, RefreshCw } from 'lucide-react';
type PendingItem = {
id: number;
domaValue: string;
suggestedId?: string | null;
suggestedName?: string | null;
createdAt: string;
};
type PendingResponse = {
success: boolean;
data: {
buildings: PendingItem[];
employees: PendingItem[];
};
};
interface DomaPendingMappingsProps {
onClose?: () => void;
}
export const DomaPendingMappings: React.FC<DomaPendingMappingsProps> = ({ onClose }) => {
const [pending, setPending] = useState<PendingResponse['data'] | null>(null);
const [buildings, setBuildings] = useState<Building[]>([]);
const [employees, setEmployees] = useState<Employee[]>([]);
const [loading, setLoading] = useState(true);
const [resolvingId, setResolvingId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const loadData = async () => {
setLoading(true);
setError(null);
try {
const [pendingRes, buildingsRes, employeesRes] = await Promise.all([
backendApi.getDomaPendingMappings(),
backendApi.getBuildings(),
backendApi.getEmployees(),
]);
if (!pendingRes.success) {
throw new Error('Ошибка при загрузке ожидающих сопоставлений');
}
setPending(pendingRes.data);
setBuildings(buildingsRes);
setEmployees(employeesRes);
} catch (e: any) {
console.error('[DomaPendingMappings] Ошибка загрузки данных:', e);
setError(e?.message || 'Ошибка загрузки данных');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
const buildingOptions = useMemo(
() =>
buildings.map((b) => ({
id: b.id,
label: b.passport?.address || 'Без адреса',
})),
[buildings]
);
const employeeOptions = useMemo(
() =>
employees.map((e) => ({
id: e.id,
label: e.name,
})),
[employees]
);
const handleResolve = async (item: PendingItem, type: 'building' | 'employee', targetId: string) => {
if (!targetId) return;
try {
setResolvingId(item.id);
await backendApi.resolveDomaPendingMapping(item.id, targetId);
window.dispatchEvent(new CustomEvent('mkd-applications-changed'));
await loadData();
} catch (e: any) {
console.error('[DomaPendingMappings] Ошибка при сопоставлении:', e);
setError(e?.message || 'Ошибка при сопоставлении');
} finally {
setResolvingId(null);
}
};
const hasData =
pending && (pending.buildings.length > 0 || pending.employees.length > 0);
return (
<div className="bg-white rounded-2xl shadow-lg border border-slate-200 p-4 md:p-6 space-y-4">
<div className="flex items-center justify-between gap-2 mb-2">
<div>
<h3 className="text-base md:text-lg font-bold text-slate-800">
Ожидающие сопоставления из Doma AI
</h3>
<p className="text-[11px] text-slate-500">
Здесь вы вручную связываете адреса и исполнителей из Doma AI с домами и
сотрудниками вашей базы. После этого синхронизация будет «чистой».
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={loadData}
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-xl text-[11px] font-bold border border-slate-200 text-slate-600 hover:bg-slate-50"
>
<RefreshCw className="w-3 h-3" /> Обновить
</button>
{onClose && (
<button
onClick={onClose}
className="text-[11px] text-slate-400 hover:text-slate-600 px-2 py-1 rounded-lg border border-transparent hover:border-slate-200"
>
Закрыть
</button>
)}
</div>
</div>
{loading && (
<div className="flex items-center justify-center py-10 text-slate-500 text-sm">
<Loader2 className="w-4 h-4 animate-spin mr-2" />
Загрузка ожидающих сопоставлений...
</div>
)}
{!loading && error && (
<div className="flex items-center gap-2 text-sm text-red-600 bg-red-50 border border-red-100 rounded-xl px-3 py-2">
<AlertTriangle className="w-4 h-4" />
<span>{error}</span>
</div>
)}
{!loading && !error && !hasData && (
<div className="text-center py-8 text-sm text-slate-500">
<p className="font-medium">Нет ожидающих сопоставлений</p>
<p className="text-xs mt-1">
Все адреса и исполнители из Doma AI уже сопоставлены с вашей базой.
</p>
</div>
)}
{!loading && !error && hasData && pending && (
<div className="space-y-6">
{pending.buildings.length > 0 && (
<section>
<h4 className="text-xs font-black text-slate-500 uppercase tracking-[0.2em] mb-2">
Адреса домов
</h4>
<div className="space-y-2">
{pending.buildings.map((item) => (
<div
key={`b-${item.id}`}
className="flex flex-col md:flex-row md:items-center gap-2 border border-slate-200 rounded-xl px-3 py-2 bg-slate-50/60"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800 truncate">
{item.domaValue}
</p>
<p className="text-[11px] text-slate-400">
из Doma AI {new Date(item.createdAt).toLocaleString()}
</p>
</div>
<div className="flex items-center gap-2 md:min-w-[260px]">
<select
className="flex-1 text-xs border border-slate-200 rounded-xl px-2 py-1.5 bg-white"
defaultValue=""
onChange={(e) =>
handleResolve(
item,
'building',
e.target.value || ''
)
}
disabled={resolvingId === item.id}
>
<option value="">Выберите дом...</option>
{buildingOptions.map((b) => (
<option key={b.id} value={b.id}>
{b.label}
</option>
))}
</select>
{resolvingId === item.id && (
<Loader2 className="w-4 h-4 text-primary-500 animate-spin" />
)}
</div>
</div>
))}
</div>
</section>
)}
{pending.employees.length > 0 && (
<section>
<h4 className="text-xs font-black text-slate-500 uppercase tracking-[0.2em] mb-2">
Исполнители (сотрудники)
</h4>
<div className="space-y-2">
{pending.employees.map((item) => (
<div
key={`e-${item.id}`}
className="flex flex-col md:flex-row md:items-center gap-2 border border-slate-200 rounded-xl px-3 py-2 bg-slate-50/60"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800 truncate">
{item.domaValue}
</p>
<p className="text-[11px] text-slate-400">
из Doma AI {new Date(item.createdAt).toLocaleString()}
</p>
</div>
<div className="flex items-center gap-2 md:min-w-[260px]">
<select
className="flex-1 text-xs border border-slate-200 rounded-xl px-2 py-1.5 bg-white"
defaultValue=""
onChange={(e) =>
handleResolve(
item,
'employee',
e.target.value || ''
)
}
disabled={resolvingId === item.id}
>
<option value="">Выберите сотрудника...</option>
{employeeOptions.map((e) => (
<option key={e.id} value={e.id}>
{e.label}
</option>
))}
</select>
{resolvingId === item.id && (
<Loader2 className="w-4 h-4 text-primary-500 animate-spin" />
)}
</div>
</div>
))}
</div>
</section>
)}
<p className="text-[11px] text-slate-400 flex items-center gap-1">
<Check className="w-3 h-3 text-emerald-500" />
После сопоставления значение будет сохранено и в следующих синхронизациях
дом/сотрудник будут подставляться автоматически.
</p>
</div>
)}
</div>
);
};