metis-desk/apps/server/src/asset-states.integration.ts
David b45925f3e2 feat(assets): implement asset picker dialog and selection components
- Added `useAssetPickerController` for managing asset selection logic.
- Created `AssetSelectProps` type for defining asset selection properties.
- Implemented `AssetPickerDialog` for displaying the asset selection interface.
- Developed `AssetPickerList` for rendering the list of selectable assets.
- Introduced `AssetSelect` component for integrating the asset picker into forms.
- Created `AssetList` component for shared asset listing functionality.
- Added tests for workstation creation logic to ensure proper validation.
- Implemented workstation asset fields and association fields for managing workstation assets.
- Developed workstation lifecycle dialog for managing work profile states.
- Added form progress component for visualizing multi-step forms.
2026-10-14 19:42:59 +02:00

213 lines
7.1 KiB
TypeScript

import assert from "node:assert/strict";
import type { AssetInput } from "@metis/assets/common";
import {
AssetResponseSchema,
InventoryResponseSchema,
WorkstationResponseSchema,
} from "@metis/assets/common";
import {
SoftwareInstallationResponseSchema,
SoftwareInstallationsResponseSchema,
} from "@metis/companies/common";
import {
CatalogResponseSchema as ResourceCatalogSchema,
ResourceResponseSchema,
} from "@metis/resources/common";
import type { Sequelize } from "sequelize";
import { QueryTypes } from "sequelize";
import { z } from "zod/v4";
import { enforceAssetRelationalConstraints } from "../../../modules/assets/src/api/infrastructure/persistence/asset-relational-constraints";
type Request = (path: string, body?: unknown) => Promise<Response>;
export async function verifyAssetStates(request: Request, companyId: string, database: Sequelize) {
const path = `/assets/${companyId}`;
const source = InventoryResponseSchema.parse(await (await request(path)).json()).assets[0];
assert.ok(source);
const response = await request(path, {
values: { ...source.values, status: "operational", name: "Estado técnico prueba" },
});
assert.equal(response.status, 201, await response.clone().text());
const original = AssetResponseSchema.parse(await response.json()).asset;
const current = async () => {
const asset = InventoryResponseSchema.parse(await (await request(path)).json()).assets.find(
(item) => item.id === original.id
);
assert.ok(asset);
return asset;
};
const state = async (status: AssetInput["status"]) => {
const previous = await current();
const result = await request(`${path}/${original.id}/update`, {
values: { ...previous.values, status },
});
assert.equal(result.status, 200, await result.clone().text());
return AssetResponseSchema.parse(await result.json()).asset;
};
const stationValues = {
name: "Asignación prueba",
active: true,
siteId: null,
personId: null,
assetIds: [original.id],
};
const station = async (name: string) => {
const result = await request(`${path}/workstations`, { values: { ...stationValues, name } });
assert.equal(result.status, 201, await result.clone().text());
return WorkstationResponseSchema.parse(await result.json()).workstation;
};
assert.equal(original.displayStatus, "available");
assert.equal(original.assigned, false);
const first = await station("Puesto de prueba");
const assigned = await current();
assert.equal(assigned.values.status, "operational");
assert.equal(assigned.displayStatus, "in_use");
assert.equal(assigned.assigned, true);
assert.equal(assigned.history.length, 1);
const repairing = await state("maintenance");
assert.equal(repairing.displayStatus, "maintenance");
assert.equal(repairing.assigned, true);
const resourceCatalog = ResourceCatalogSchema.parse(
await (await request(`/resource-catalog/${companyId}`)).json()
);
const osTemplate = resourceCatalog.items.find((item) => item.builtinKey === "template:os");
assert.ok(osTemplate);
const osResponse = await request(`/resources/${companyId}`, {
values: {
name: "SO durante reparación",
templateId: osTemplate.id,
active: true,
notes: "",
characteristics: {},
},
});
assert.equal(osResponse.status, 201);
const os = ResourceResponseSchema.parse(await osResponse.json()).resource;
const installationResponse = await request(`/software-installations/${companyId}`, {
values: {
assetId: original.id,
resourceId: os.id,
licenseResourceId: null,
version: "24H2",
state: "configured",
notes: null,
},
});
assert.equal(installationResponse.status, 201, await installationResponse.clone().text());
const installation = SoftwareInstallationResponseSchema.parse(
await installationResponse.json()
).configuration;
assert.equal(installation.needsReview, false);
assert.equal(
(
await request(`${path}/workstations/${first.id}/update`, {
values: { ...first.values, name: "Puesto durante reparación" },
})
).status,
200
);
for (const status of ["maintenance", "broken"] as const) {
await state(status);
const blocked = await request(`${path}/workstations`, {
values: { ...stationValues, name: "Asignación no permitida" },
});
assert.equal(blocked.status, 409);
assert.equal(
z.object({ code: z.string() }).parse(await blocked.json()).code,
"ASSET_NOT_OPERATIONAL"
);
}
const retiredWhileAssigned = await request(`${path}/${original.id}/update`, {
values: { ...(await current()).values, status: "retired" },
});
assert.equal(retiredWhileAssigned.status, 409);
assert.equal(
z.object({ code: z.string() }).parse(await retiredWhileAssigned.json()).code,
"ASSET_ASSIGNED"
);
assert.equal((await current()).values.status, "broken");
await state("operational");
assert.equal((await request(`${path}/workstations/${first.id}/decommission`, {})).status, 200);
assert.equal((await request(`${path}/workstations/${first.id}/delete`, {})).status, 200);
assert.equal((await current()).displayStatus, "available");
assert.equal((await current()).assigned, false);
const retired = await state("retired");
assert.equal(retired.displayStatus, "retired");
const installations = SoftwareInstallationsResponseSchema.parse(
await (await request(`/software-installations/${companyId}`)).json()
);
assert.equal(
installations.configurations.find((item) => item.id === installation.id)?.needsReview,
true
);
assert.equal((await request(`${path}/workstations`, { values: stationValues })).status, 409);
for (const status of ["in_use", "available"]) {
assert.equal(
(await request(`${path}/${original.id}/update`, { values: { ...retired.values, status } }))
.status,
400
);
}
// Datos históricos originales: no se convierten en asignaciones ficticias ni se reescriben.
await state("operational");
const before = await current();
const revisionId = before.history.at(-1)?.id;
assert.ok(revisionId);
await database.transaction(async (transaction) => {
await database.query("UPDATE assets SET status='in_use' WHERE id=:id", {
replacements: { id: original.id },
transaction,
});
await database.query("UPDATE asset_revisions SET status='in_use' WHERE id=:id", {
replacements: { id: revisionId },
transaction,
});
});
await enforceAssetRelationalConstraints(database);
const historical = await current();
assert.equal(historical.values.status, "operational");
assert.equal(historical.displayStatus, "available");
assert.equal(historical.history.at(-1)?.values.status, "in_use");
const repeated = await request(`${path}/${original.id}/update`, { values: historical.values });
assert.equal(repeated.status, 200);
assert.deepEqual(
AssetResponseSchema.parse(await repeated.json()).asset.history,
historical.history
);
const stored = await database.query<{ status: string }>(
"SELECT status FROM asset_revisions WHERE id=:id",
{ replacements: { id: revisionId }, type: QueryTypes.SELECT }
);
assert.equal(stored[0]?.status, "in_use");
}