feat(inventory): add function catalog and act data
DH V2 CI / WEB · typecheck, build (push) Successful in 45s
Production dependency audit / API · production dependencies (push) Successful in 27s
DH V2 CI / API · typecheck, tests, build (push) Successful in 1m15s
Production dependency audit / WEB · production dependencies (push) Successful in 15s
DH V2 CI / Docker / migrations / production images (push) Successful in 2m21s
DH V2 CI / Promote verified main to deploy (push) Successful in 9s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Failing after 6m25s
DH V2 CI / WEB · typecheck, build (push) Successful in 45s
Production dependency audit / API · production dependencies (push) Successful in 27s
DH V2 CI / API · typecheck, tests, build (push) Successful in 1m15s
Production dependency audit / WEB · production dependencies (push) Successful in 15s
DH V2 CI / Docker / migrations / production images (push) Successful in 2m21s
DH V2 CI / Promote verified main to deploy (push) Successful in 9s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Failing after 6m25s
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dhv2-web",
|
||||
"version": "0.23.0-13",
|
||||
"version": "0.23.0-14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dhv2-web",
|
||||
"version": "0.23.0-13",
|
||||
"version": "0.23.0-14",
|
||||
"dependencies": {
|
||||
"maplibre-gl": "6.4.1",
|
||||
"react": "^19.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dhv2-web",
|
||||
"version": "0.23.0-13",
|
||||
"version": "0.23.0-14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -35,6 +35,7 @@ import { VerificationPlanningPage } from '../pages/VerificationPlanningPage';
|
||||
import { ActAdministrationPage } from '../pages/ActAdministrationPage';
|
||||
import { ActAdministrationDetailPage } from '../pages/ActAdministrationDetailPage';
|
||||
import { FieldBriefingsPage } from '../pages/FieldBriefingsPage';
|
||||
import { InventoryFunctionsPage } from '../pages/InventoryFunctionsPage';
|
||||
|
||||
const MapPage = lazy(() => import('../pages/MapPage').then((module) => ({ default: module.MapPage })));
|
||||
const DocumentDeliveryPage = lazy(() => import('../pages/DocumentDeliveryPage').then((module) => ({ default: module.DocumentDeliveryPage })));
|
||||
@@ -82,6 +83,7 @@ export function App() {
|
||||
<Route element={<PermissionRoute permission="roles.read" />}><Route path="/admin/roles" element={<RolesPage />} /></Route>
|
||||
<Route element={<PermissionRoute permission="audit.read" />}><Route path="/admin/audit" element={<AuditPage />} /></Route>
|
||||
<Route element={<PermissionRoute permission="asset_types.read" />}><Route path="/admin/asset-types" element={<AuthoritativeInventoryConfigPage />} /></Route>
|
||||
<Route element={<PermissionRoute permission="asset_types.manage" />}><Route path="/admin/inventory-functions" element={<InventoryFunctionsPage />} /></Route>
|
||||
<Route element={<PermissionRoute permission="finding_catalog.manage" />}><Route path="/admin/finding-catalog" element={<FindingCatalogPage />} /></Route>
|
||||
<Route element={<PermissionRoute permission="document_delivery.read" />}><Route path="/admin/document-delivery" element={<DocumentDeliveryPage />} /></Route>
|
||||
<Route path="/sin-acceso" element={<AccessDeniedPage />} />
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const APP_VERSION = '0.23.0-13';
|
||||
export const APP_PHASE = 'F6.17 · Tablas ordenables';
|
||||
export const APP_VERSION = '0.23.0-14';
|
||||
export const APP_PHASE = 'F6.18 · Catálogo de funciones';
|
||||
|
||||
@@ -29,7 +29,7 @@ function normalized(value: string): string {
|
||||
|
||||
function supportsFunctionChange(code: string, name: string): boolean {
|
||||
const value = `${normalized(code)} ${normalized(name)}`;
|
||||
return value.split(' ').some((part) => part === 'estacion' || part === 'subestacion');
|
||||
return value.split(' ').some((part) => part === 'instalacion' || part === 'subinstalacion');
|
||||
}
|
||||
|
||||
export function AssetHistoryPanel({
|
||||
|
||||
@@ -85,7 +85,7 @@ export function InventoryFunctionPanel({ assetId }: { assetId: string }) {
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<span className="eyebrow">FUNCIÓN OPERATIVA</span>
|
||||
<h2>Función de la Estación / Subestación</h2>
|
||||
<h2>Función de la Instalación / Subinstalación</h2>
|
||||
<p className="section-copy">La identidad del Inventario no cambia. Cada mutación de función conserva fecha, usuario y antecedente para la consulta histórica.</p>
|
||||
</div>
|
||||
{canManageCatalog && <Link className="button secondary" to="/admin/inventory-functions">Administrar catálogo</Link>}
|
||||
@@ -110,7 +110,7 @@ export function InventoryFunctionPanel({ assetId }: { assetId: string }) {
|
||||
{canChange && <form className="form-section" onSubmit={submit}>
|
||||
<div>
|
||||
<h3>Cambiar función</h3>
|
||||
<p className="section-copy">Ejemplo: Bombeo Mecánico AIB → Bombeo Mecánico Rotaflex. No se crea una Estación nueva.</p>
|
||||
<p className="section-copy">La Función cambia sin crear un nuevo registro de Inventario y conserva su historial.</p>
|
||||
</div>
|
||||
<div className="form-grid">
|
||||
<label className="field">
|
||||
|
||||
@@ -35,6 +35,7 @@ const administration: NavItem[] = [
|
||||
{ to: '/admin/users', label: 'Usuarios', icon: 'users', permission: 'users.read' },
|
||||
{ to: '/admin/roles', label: 'Roles y permisos', icon: 'shield', permission: 'roles.read' },
|
||||
{ to: '/admin/asset-types', label: 'Configuración de Inventarios', icon: 'layers', permission: 'asset_types.read' },
|
||||
{ to: '/admin/inventory-functions', label: 'Catálogo de funciones', icon: 'layers', permission: 'asset_types.manage' },
|
||||
{ to: '/admin/finding-catalog', label: 'Catálogo de hallazgos', icon: 'alert', permission: 'finding_catalog.manage' },
|
||||
{ to: '/admin/document-delivery', label: 'Entrega documental', icon: 'audit', permission: 'document_delivery.read' },
|
||||
];
|
||||
|
||||
@@ -2061,6 +2061,7 @@ export function createAsset(input: {
|
||||
code: string;
|
||||
name: string;
|
||||
commonName?: string | null;
|
||||
functionId?: string | null;
|
||||
typeId: string;
|
||||
parentId?: string | null;
|
||||
operationalAreaId?: string | null;
|
||||
|
||||
@@ -50,7 +50,7 @@ export function getInventoryFunctionHistory(assetId: string) {
|
||||
|
||||
export function changeInventoryFunction(
|
||||
assetId: string,
|
||||
input: { functionId: string; effectiveAt?: string; reason?: string | null },
|
||||
input: { functionId: string | null; effectiveAt?: string; reason?: string | null },
|
||||
) {
|
||||
return apiRequest<InventoryFunctionHistory>(`/assets/${assetId}/function`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
updateAssetInformationStatus,
|
||||
updateAssetOperationalStatus,
|
||||
} from '../lib/api';
|
||||
import { changeInventoryFunction, getInventoryFunctionHistory, listInventoryFunctions } from '../lib/inventoryFunctionApi';
|
||||
import type { InventoryFunction } from '../lib/inventoryFunctionApi';
|
||||
import type {
|
||||
AssetAttributeDefinition,
|
||||
AssetDetail,
|
||||
@@ -140,6 +142,9 @@ export function AssetEditorPage() {
|
||||
const [operationalAreas, setOperationalAreas] = useState<OperationalAssetSummary[]>([]);
|
||||
const [operationalCompanies, setOperationalCompanies] = useState<OperationalAssetSummary[]>([]);
|
||||
const [description, setDescription] = useState('');
|
||||
const [functionCatalog, setFunctionCatalog] = useState<InventoryFunction[]>([]);
|
||||
const [functionId, setFunctionId] = useState('');
|
||||
const [currentFunctionId, setCurrentFunctionId] = useState('');
|
||||
const [status, setStatus] = useState<AssetInformationStatus>('DRAFT');
|
||||
const [operationalStatus, setOperationalStatus] = useState<AssetOperationalStatus>('UNKNOWN');
|
||||
const [attributeValues, setAttributeValues] = useState<Record<string, unknown>>({});
|
||||
@@ -154,9 +159,21 @@ export function AssetEditorPage() {
|
||||
|
||||
const selectedType = types.find((type) => type.id === typeId) ?? null;
|
||||
const definitions = useMemo(
|
||||
() => selectedType?.attributes.filter((item) => item.isActive) ?? [],
|
||||
() => selectedType?.attributes.filter((item) => item.isActive && item.code !== 'campo_funcion') ?? [],
|
||||
[selectedType],
|
||||
);
|
||||
const structuralTypeCode = normalizeStructuralType(selectedType?.code);
|
||||
const structuralTypeName = normalizeStructuralType(selectedType?.name);
|
||||
const technicalInventory = structuralTypeCode === 'instalacion'
|
||||
|| structuralTypeCode === 'subinstalacion'
|
||||
|| structuralTypeName === 'instalacion'
|
||||
|| structuralTypeName === 'subinstalacion';
|
||||
|
||||
useEffect(() => {
|
||||
listInventoryFunctions()
|
||||
.then(setFunctionCatalog)
|
||||
.catch((requestError) => setError(errorMessage(requestError)));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@@ -215,6 +232,20 @@ export function AssetEditorPage() {
|
||||
.catch((requestError) => setError(errorMessage(requestError)));
|
||||
}, [editing, contextParentId, types]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editing || !id || !technicalInventory || !canReadHistory) {
|
||||
if (!editing) { setFunctionId(''); setCurrentFunctionId(''); }
|
||||
return;
|
||||
}
|
||||
getInventoryFunctionHistory(id)
|
||||
.then((history) => {
|
||||
const value = history.currentFunction?.functionId ?? '';
|
||||
setFunctionId(value);
|
||||
setCurrentFunctionId(value);
|
||||
})
|
||||
.catch((requestError) => setError(errorMessage(requestError)));
|
||||
}, [editing, id, technicalInventory, canReadHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!typeId) {
|
||||
setParents([]);
|
||||
@@ -274,6 +305,8 @@ export function AssetEditorPage() {
|
||||
setOperationalAreaId('');
|
||||
if (!editing) setOperatorCompanyId('');
|
||||
setAttributeValues({});
|
||||
setFunctionId('');
|
||||
setCurrentFunctionId('');
|
||||
setParentSearch('');
|
||||
};
|
||||
|
||||
@@ -287,14 +320,19 @@ export function AssetEditorPage() {
|
||||
setError('');
|
||||
setSuccess('');
|
||||
try {
|
||||
if (technicalInventory && !commonName.trim()) {
|
||||
setError('Completá el Nombre habitual. Es obligatorio para Instalaciones y Subinstalaciones.');
|
||||
return;
|
||||
}
|
||||
const attributes = normalizeAttributeValues(definitions, attributeValues);
|
||||
const nameToSave = technicalInventory ? (name.trim() || commonName.trim()) : name.trim();
|
||||
let saved: AssetDetail;
|
||||
if (editing && id) {
|
||||
if (canEdit) {
|
||||
saved = await updateAsset(id, {
|
||||
typeId: canDirectContextEdit ? typeId : undefined,
|
||||
code,
|
||||
name,
|
||||
name: nameToSave,
|
||||
commonName: commonName.trim() || null,
|
||||
parentId: canDirectContextEdit ? parentId || null : undefined,
|
||||
operationalAreaId: canDirectContextEdit ? operationalAreaId || null : undefined,
|
||||
@@ -310,14 +348,21 @@ export function AssetEditorPage() {
|
||||
if (canChangeOperationalStatus && saved.operationalStatus !== operationalStatus) {
|
||||
saved = await updateAssetOperationalStatus(id, operationalStatus);
|
||||
}
|
||||
if (technicalInventory && functionId !== currentFunctionId) {
|
||||
const functionHistory = await changeInventoryFunction(id, { functionId: functionId || null });
|
||||
const nextFunctionId = functionHistory.currentFunction?.functionId ?? '';
|
||||
setCurrentFunctionId(nextFunctionId);
|
||||
setFunctionId(nextFunctionId);
|
||||
}
|
||||
setAsset(saved);
|
||||
setSuccess('Registro actualizado correctamente');
|
||||
setHistoryRefreshKey((current) => current + 1);
|
||||
} else {
|
||||
saved = await createAsset({
|
||||
code,
|
||||
name,
|
||||
name: nameToSave,
|
||||
commonName: commonName.trim() || null,
|
||||
functionId: technicalInventory ? (functionId || null) : null,
|
||||
typeId,
|
||||
parentId: parentId || null,
|
||||
operationalAreaId: operationalAreaId || null,
|
||||
@@ -351,8 +396,6 @@ export function AssetEditorPage() {
|
||||
? 'companies'
|
||||
: 'territory';
|
||||
|
||||
const structuralTypeCode = normalizeStructuralType(selectedType?.code);
|
||||
const structuralTypeName = normalizeStructuralType(selectedType?.name);
|
||||
const compactStructuralSummary = editing && (
|
||||
['instalacion', 'instalacion-superficie', 'instalacion_de_superficie', 'instalacion-de-superficie', 'subinstalacion']
|
||||
.includes(structuralTypeCode)
|
||||
@@ -462,24 +505,13 @@ export function AssetEditorPage() {
|
||||
|
||||
<div className="form-grid asset-compact-primary-grid">
|
||||
<label className="field">
|
||||
<span>Nombre técnico</span>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
disabled={!canEdit}
|
||||
required
|
||||
maxLength={200}
|
||||
/>
|
||||
<span>Nombre habitual <em>obligatorio</em></span>
|
||||
<input value={commonName} onChange={(event) => setCommonName(event.target.value)} disabled={!canEdit} required maxLength={200} placeholder="Nombre usado habitualmente en campo" />
|
||||
<small>Es el nombre que se mostrará en el Acta.</small>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Nombre habitual / sobrenombre <em>opcional</em></span>
|
||||
<input
|
||||
value={commonName}
|
||||
onChange={(event) => setCommonName(event.target.value)}
|
||||
disabled={!canEdit}
|
||||
maxLength={200}
|
||||
placeholder="Ej.: planta vieja, celda principal…"
|
||||
/>
|
||||
<span>Nombre técnico <em>opcional</em></span>
|
||||
<input value={name} onChange={(event) => setName(event.target.value)} disabled={!canEdit} maxLength={200} placeholder="Denominación técnica, si corresponde" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -628,6 +660,15 @@ export function AssetEditorPage() {
|
||||
</label>}
|
||||
</div>}
|
||||
|
||||
{technicalInventory && <label className="field">
|
||||
<span>Función <em>opcional</em></span>
|
||||
<SearchableSelect value={functionId} onChange={(event) => setFunctionId(event.target.value)} disabled={!canEdit}>
|
||||
<option value="">Sin función asignada</option>
|
||||
{functionCatalog.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</SearchableSelect>
|
||||
{hasPermission('asset_types.manage') && <small><Link to="/admin/inventory-functions" className="text-link">Administrar Catálogo de funciones</Link></small>}
|
||||
</label>}
|
||||
|
||||
<label className="field">
|
||||
<span>Descripción <em>opcional</em></span>
|
||||
<textarea
|
||||
@@ -671,21 +712,24 @@ export function AssetEditorPage() {
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Nombre técnico</span>
|
||||
<input value={name} onChange={(event) => setName(event.target.value)} disabled={!canEdit} required maxLength={200} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Nombre habitual / sobrenombre <em>opcional</em></span>
|
||||
<input
|
||||
value={commonName}
|
||||
onChange={(event) => setCommonName(event.target.value)}
|
||||
disabled={!canEdit}
|
||||
maxLength={200}
|
||||
placeholder="Ej.: tanque grande, ET vieja, batería norte…"
|
||||
/>
|
||||
<small>También se usa en las búsquedas del Inventario.</small>
|
||||
<span>{technicalInventory ? <>Nombre habitual <em>obligatorio</em></> : 'Nombre'}</span>
|
||||
<input value={technicalInventory ? commonName : name} onChange={(event) => technicalInventory ? setCommonName(event.target.value) : setName(event.target.value)} disabled={!canEdit} required maxLength={200} placeholder={technicalInventory ? 'Nombre usado habitualmente en campo' : undefined} />
|
||||
{technicalInventory && <small>Es el nombre que se mostrará en el Acta.</small>}
|
||||
</label>
|
||||
{technicalInventory && <label className="field">
|
||||
<span>Nombre técnico <em>opcional</em></span>
|
||||
<input value={name} onChange={(event) => setName(event.target.value)} disabled={!canEdit} maxLength={200} placeholder="Denominación técnica, si corresponde" />
|
||||
</label>}
|
||||
</div>
|
||||
{technicalInventory && <label className="field">
|
||||
<span>Función <em>opcional</em></span>
|
||||
<SearchableSelect value={functionId} onChange={(event) => setFunctionId(event.target.value)} disabled={!canEdit}>
|
||||
<option value="">Sin función asignada</option>
|
||||
{functionCatalog.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</SearchableSelect>
|
||||
{hasPermission('asset_types.manage') && <small><Link to="/admin/inventory-functions" className="text-link">Administrar Catálogo de funciones</Link></small>}
|
||||
</label>}
|
||||
|
||||
<label className="field">
|
||||
<span>Descripción <em>opcional</em></span>
|
||||
<textarea
|
||||
|
||||
@@ -70,6 +70,7 @@ export function DashboardPage() {
|
||||
<div className="quick-links">
|
||||
<Link to="/inventarios"><Icon name="layers" /><span><strong>Inventarios</strong><small>Instancias reales organizadas por empresa, área y jerarquía</small></span><Icon name="chevron" /></Link>
|
||||
{hasPermission('asset_types.read') && <Link to="/admin/asset-types"><Icon name="layers" /><span><strong>Configuración de Inventarios</strong><small>Jerarquía, tipos y campos configurables</small></span><Icon name="chevron" /></Link>}
|
||||
{hasPermission('asset_types.manage') && <Link to="/admin/inventory-functions"><Icon name="layers" /><span><strong>Catálogo de funciones</strong><small>Funciones disponibles para Instalaciones y Subinstalaciones</small></span><Icon name="chevron" /></Link>}
|
||||
{hasPermission('finding_catalog.manage') && <Link to="/admin/finding-catalog"><Icon name="alert" /><span><strong>Catálogo de hallazgos</strong><small>Hallazgos aplicables según el tipo de Subinstalación</small></span><Icon name="chevron" /></Link>}
|
||||
{hasPermission('users.read') && <Link to="/admin/users"><Icon name="users" /><span><strong>Usuarios</strong><small>Accesos y roles</small></span><Icon name="chevron" /></Link>}
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function InventoryFunctionsPage() {
|
||||
setName('');
|
||||
setDescription('');
|
||||
setSortOrder('0');
|
||||
setSuccess('Función incorporada al catálogo. Ya puede asignarse a Estaciones y Subestaciones.');
|
||||
setSuccess('Función incorporada al catálogo. Ya puede asignarse a Instalaciones y Subinstalaciones.');
|
||||
load();
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
@@ -112,7 +112,7 @@ export function InventoryFunctionsPage() {
|
||||
|
||||
return <section className="narrow-section">
|
||||
<div className="page-heading">
|
||||
<div><span className="eyebrow">ADMINISTRACIÓN</span><h1>Catálogo de funciones</h1><p>Funciones operativas que pueden asumir las Estaciones y Subestaciones. Desactivar una opción nunca borra su uso histórico.</p></div>
|
||||
<div><span className="eyebrow">ADMINISTRACIÓN</span><h1>Catálogo de funciones</h1><p>Funciones que pueden asignarse a Instalaciones y Subinstalaciones. Desactivar una opción nunca borra su uso histórico.</p></div>
|
||||
</div>
|
||||
|
||||
{error && <Alert>{error}</Alert>}
|
||||
|
||||
Reference in New Issue
Block a user