F5.1: filtrar Hallazgos por clasificación seleccionada
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { SearchableSelect } from '../../components/SearchableSelect';
|
import { SearchableSelect } from '../../components/SearchableSelect';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
import { Alert, LoadingBlock, errorMessage } from '../../components/Feedback';
|
import { Alert, LoadingBlock, errorMessage } from '../../components/Feedback';
|
||||||
import { Icon } from '../../components/Icon';
|
import { Icon } from '../../components/Icon';
|
||||||
import { getFindingCatalogAdmin } from '../../lib/api';
|
import { getFindingCatalogAdmin } from '../../lib/api';
|
||||||
@@ -12,19 +13,25 @@ import type { InventoryFamily } from '../../lib/inventoryStructureApi';
|
|||||||
|
|
||||||
const EMPTY_CATALOG: FindingAdminCatalog = { categories: [], items: [] };
|
const EMPTY_CATALOG: FindingAdminCatalog = { categories: [], items: [] };
|
||||||
|
|
||||||
|
type ViewMode = 'ASSOCIATED' | 'ALL';
|
||||||
|
|
||||||
export function FindingCatalogTypeApplicabilityPanel() {
|
export function FindingCatalogTypeApplicabilityPanel() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const requestedFamilyId = searchParams.get('familyId') ?? '';
|
||||||
const [families, setFamilies] = useState<InventoryFamily[]>([]);
|
const [families, setFamilies] = useState<InventoryFamily[]>([]);
|
||||||
const [catalog, setCatalog] = useState<FindingAdminCatalog>(EMPTY_CATALOG);
|
const [catalog, setCatalog] = useState<FindingAdminCatalog>(EMPTY_CATALOG);
|
||||||
const [familyId, setFamilyId] = useState('');
|
const [familyId, setFamilyId] = useState('');
|
||||||
const [enabled, setEnabled] = useState<Set<string>>(new Set());
|
const [enabled, setEnabled] = useState<Set<string>>(new Set());
|
||||||
const [reason, setReason] = useState('Actualización de aplicabilidad por clasificación de Inventario');
|
const [reason, setReason] = useState('Actualización de Hallazgos asociados a la clasificación de Inventario');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>('ASSOCIATED');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
|
|
||||||
const selectedFamily = families.find((family) => family.id === familyId) ?? null;
|
const selectedFamily = families.find((family) => family.id === familyId) ?? null;
|
||||||
|
const savedIds = useMemo(() => new Set(selectedFamily?.findingItemIds ?? []), [selectedFamily]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([listInventoryFamiliesAdmin(), getFindingCatalogAdmin()])
|
Promise.all([listInventoryFamiliesAdmin(), getFindingCatalogAdmin()])
|
||||||
@@ -32,15 +39,19 @@ export function FindingCatalogTypeApplicabilityPanel() {
|
|||||||
const activeFamilies = loadedFamilies.filter((family) => family.isActive !== false);
|
const activeFamilies = loadedFamilies.filter((family) => family.isActive !== false);
|
||||||
setFamilies(activeFamilies);
|
setFamilies(activeFamilies);
|
||||||
setCatalog(loadedCatalog);
|
setCatalog(loadedCatalog);
|
||||||
setFamilyId(activeFamilies[0]?.id ?? '');
|
setFamilyId(activeFamilies.some((family) => family.id === requestedFamilyId)
|
||||||
|
? requestedFamilyId
|
||||||
|
: activeFamilies[0]?.id ?? '');
|
||||||
})
|
})
|
||||||
.catch((requestError) => setError(errorMessage(requestError)))
|
.catch((requestError) => setError(errorMessage(requestError)))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, [requestedFamilyId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selected = families.find((family) => family.id === familyId);
|
const selected = families.find((family) => family.id === familyId);
|
||||||
setEnabled(new Set(selected?.findingItemIds ?? []));
|
setEnabled(new Set(selected?.findingItemIds ?? []));
|
||||||
|
setViewMode('ASSOCIATED');
|
||||||
|
setSearch('');
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
setError('');
|
setError('');
|
||||||
}, [familyId, families]);
|
}, [familyId, families]);
|
||||||
@@ -53,7 +64,7 @@ export function FindingCatalogTypeApplicabilityPanel() {
|
|||||||
catalog.categories.map((category) => [category.id, category.name]),
|
catalog.categories.map((category) => [category.id, category.name]),
|
||||||
), [catalog.categories]);
|
), [catalog.categories]);
|
||||||
|
|
||||||
const visible = useMemo(() => {
|
const available = useMemo(() => {
|
||||||
const needle = search.trim().toLocaleLowerCase('es-AR');
|
const needle = search.trim().toLocaleLowerCase('es-AR');
|
||||||
return catalog.items.filter((item) =>
|
return catalog.items.filter((item) =>
|
||||||
item.isActive
|
item.isActive
|
||||||
@@ -63,12 +74,23 @@ export function FindingCatalogTypeApplicabilityPanel() {
|
|||||||
);
|
);
|
||||||
}, [catalog.items, search, activeCategoryIds, categoryName]);
|
}, [catalog.items, search, activeCategoryIds, categoryName]);
|
||||||
|
|
||||||
|
const visible = useMemo(() => viewMode === 'ASSOCIATED'
|
||||||
|
? available.filter((item) => savedIds.has(item.id))
|
||||||
|
: available,
|
||||||
|
[available, savedIds, viewMode]);
|
||||||
|
|
||||||
const toggle = (id: string) => setEnabled((current) => {
|
const toggle = (id: string) => setEnabled((current) => {
|
||||||
const next = new Set(current);
|
const next = new Set(current);
|
||||||
if (next.has(id)) next.delete(id); else next.add(id);
|
if (next.has(id)) next.delete(id); else next.add(id);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectVisible = () => setEnabled((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
visible.forEach((item) => next.add(item.id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!selectedFamily) return;
|
if (!selectedFamily) return;
|
||||||
setSaving(true); setError(''); setSuccess('');
|
setSaving(true); setError(''); setSuccess('');
|
||||||
@@ -82,6 +104,7 @@ export function FindingCatalogTypeApplicabilityPanel() {
|
|||||||
setFamilies((current) => current.map((family) => family.id === selectedFamily.id
|
setFamilies((current) => current.map((family) => family.id === selectedFamily.id
|
||||||
? { ...family, findingItemIds: itemIds, findingCount: itemIds.length }
|
? { ...family, findingItemIds: itemIds, findingCount: itemIds.length }
|
||||||
: family));
|
: family));
|
||||||
|
setViewMode('ASSOCIATED');
|
||||||
setSuccess(`Hallazgos guardados para ${selectedFamily.name}.`);
|
setSuccess(`Hallazgos guardados para ${selectedFamily.name}.`);
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
setError(errorMessage(requestError));
|
setError(errorMessage(requestError));
|
||||||
@@ -98,9 +121,9 @@ export function FindingCatalogTypeApplicabilityPanel() {
|
|||||||
<div>
|
<div>
|
||||||
<span className="eyebrow">APLICABILIDAD POR INSTALACIÓN / SUBINSTALACIÓN</span>
|
<span className="eyebrow">APLICABILIDAD POR INSTALACIÓN / SUBINSTALACIÓN</span>
|
||||||
<h2>Qué Hallazgos verá el inspector</h2>
|
<h2>Qué Hallazgos verá el inspector</h2>
|
||||||
<p className="section-copy">Los Hallazgos se vinculan a la clasificación concreta del elemento, no a una “función”. La opción OTROS permanece siempre disponible en la APK.</p>
|
<p className="section-copy">Al elegir una clasificación se muestran primero sólo sus Hallazgos asociados. Usá “Todos para vincular” únicamente cuando quieras agregar o quitar asociaciones.</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="count-pill">{enabled.size} vinculados</span>
|
<span className="count-pill">{savedIds.size} asociados</span>
|
||||||
</div>
|
</div>
|
||||||
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
|
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
|
||||||
<div className="form-grid finding-applicability-toolbar">
|
<div className="form-grid finding-applicability-toolbar">
|
||||||
@@ -116,17 +139,30 @@ export function FindingCatalogTypeApplicabilityPanel() {
|
|||||||
</div>
|
</div>
|
||||||
{selectedFamily && <div className="temporal-notice">
|
{selectedFamily && <div className="temporal-notice">
|
||||||
<Icon name="layers" />
|
<Icon name="layers" />
|
||||||
<p><strong>{selectedFamily.level === 'INSTALLATION' ? 'Instalación' : 'Subinstalación'}:</strong> {selectedFamily.parentFamilyName ? `${selectedFamily.parentFamilyName} → ` : ''}{selectedFamily.name}. Actualmente tiene {selectedFamily.findingCount ?? enabled.size} Hallazgo{(selectedFamily.findingCount ?? enabled.size) === 1 ? '' : 's'} asociado{(selectedFamily.findingCount ?? enabled.size) === 1 ? '' : 's'}.</p>
|
<p><strong>{selectedFamily.level === 'INSTALLATION' ? 'Instalación' : 'Subinstalación'}:</strong> {selectedFamily.parentFamilyName ? `${selectedFamily.parentFamilyName} → ` : ''}{selectedFamily.name}. Tiene {savedIds.size} Hallazgo{savedIds.size === 1 ? '' : 's'} asociado{savedIds.size === 1 ? '' : 's'}.</p>
|
||||||
</div>}
|
</div>}
|
||||||
<div className="catalog-selection-actions">
|
|
||||||
<button type="button" className="button secondary" onClick={() => setEnabled(new Set(visible.map((item) => item.id)))}>Seleccionar visibles</button>
|
<div className="quick-view-row" style={{ marginBottom: 12 }}>
|
||||||
<button type="button" className="button secondary" onClick={() => setEnabled(new Set())}>Quitar todos</button>
|
<button type="button" className={viewMode === 'ASSOCIATED' ? 'active' : ''} onClick={() => setViewMode('ASSOCIATED')}>Asociados ({savedIds.size})</button>
|
||||||
|
<button type="button" className={viewMode === 'ALL' ? 'active' : ''} onClick={() => setViewMode('ALL')}>Todos para vincular ({available.length})</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="finding-selection-list">{visible.map((item) => <label className="finding-selection-row" key={item.id}>
|
|
||||||
<input type="checkbox" checked={enabled.has(item.id)} onChange={() => toggle(item.id)} />
|
{viewMode === 'ASSOCIATED' && visible.length === 0
|
||||||
<span><strong>{item.title}</strong><small>{categoryName.get(item.categoryId) ?? 'Catálogo'} · {item.code}{item.suggestedSeverity ? ` · gravedad sugerida ${item.suggestedSeverity}/10` : ''}</small></span>
|
? <div className="inline-empty"><strong>Esta clasificación todavía no tiene Hallazgos asociados.</strong><br />Abrí “Todos para vincular” para elegirlos.</div>
|
||||||
</label>)}</div>
|
: <>
|
||||||
<label className="field"><span>Motivo del cambio</span><textarea rows={2} value={reason} onChange={(event) => setReason(event.target.value)} maxLength={2000} placeholder="Ej.: Hallazgos aplicables a esta Subinstalación según criterio técnico…" /></label>
|
{viewMode === 'ALL' && <div className="catalog-selection-actions">
|
||||||
<div className="form-actions"><button type="button" className="button primary" disabled={saving || reason.trim().length < 5 || !selectedFamily} onClick={save}><Icon name="check" />{saving ? 'Guardando…' : 'Guardar Hallazgos vinculados'}</button></div>
|
<button type="button" className="button secondary" onClick={selectVisible}>Seleccionar visibles</button>
|
||||||
|
<button type="button" className="button secondary" onClick={() => setEnabled(new Set())}>Quitar todos</button>
|
||||||
|
</div>}
|
||||||
|
<div className="finding-selection-list">{visible.map((item) => <label className="finding-selection-row" key={item.id}>
|
||||||
|
<input type="checkbox" checked={enabled.has(item.id)} onChange={() => toggle(item.id)} />
|
||||||
|
<span><strong>{item.title}</strong><small>{categoryName.get(item.categoryId) ?? 'Catálogo'} · {item.code}{item.suggestedSeverity ? ` · gravedad sugerida ${item.suggestedSeverity}/10` : ''}</small></span>
|
||||||
|
</label>)}</div>
|
||||||
|
</>}
|
||||||
|
|
||||||
|
{viewMode === 'ALL' && <>
|
||||||
|
<label className="field"><span>Motivo del cambio</span><textarea rows={2} value={reason} onChange={(event) => setReason(event.target.value)} maxLength={2000} /></label>
|
||||||
|
<div className="form-actions"><button type="button" className="button primary" disabled={saving || reason.trim().length < 5 || !selectedFamily} onClick={save}><Icon name="check" />{saving ? 'Guardando…' : `Guardar ${enabled.size} asociados`}</button></div>
|
||||||
|
</>}
|
||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user